文章目录
Java中的死锁及其避免方法:
1、死锁定义: 多个线程因互相等待对方持有的锁而无法继续执行。
2、避免方法:
-
避免一个线程同时获取多个锁。
-
设置锁获取的超时时间。
-
按顺序申请资源。
📙 作者: 编程技术圈(哇哥就业陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta
零、引入
“服务器卡死了!CPU 100%,订单全卡着不动!” 技术部炸开锅,王二写的订单扣库存代码上线 10 分钟就把生产服务器干趴了,重启后没过 5 分钟又卡死。领导黑着脸把监控甩他脸上:“查不出问题,今天直接卷铺盖走!”
王二扒着代码看了半天,扣库存和扣余额的逻辑都加了锁,单测时顺风顺水,一到线上并发就僵死。隔壁哇哥端着泡满枸杞的保温杯过来,敲了敲键盘,用 jstack 命令调出一行红色日志:“你这是典型的死锁 —— 线程 A 拿着库存锁等余额锁,线程 B 拿着余额锁等库存锁,俩线程互相耗着,谁也不让谁,CPU 全空转了。今天把死锁搞懂,不仅能救你的工作,下次面试被问,你能把面试官讲懵。”
点赞 + 关注跟着哇哥和王二,用抢杯子和笔的例子搞懂死锁,3 招避免死锁的代码直接抄,还教你线上排查死锁的绝招!

一、先复现!王二的死锁代码,服务器卡死的元凶
王二的订单处理逻辑很简单:用户下单时,先扣商品库存,再扣用户余额,为了保证线程安全,他给库存和余额各加了一把锁。单线程测试时没问题,线上 100 个并发订单一来,直接卡死。
✅王二的 “锁死” 代码(必踩坑版)

package cn.tcmeta.deadlock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author: laoren
* @description: 死锁
* @version: 1.0.0
*/
// 订单处理系统——死锁版
public class DeadLockSample {
// 库存锁:保护库存变量
static final Object STOCK_LOCK = new Object();
// 余额锁:保护余额变量
static final Object BALANCE_LOCK = new Object();
// 模拟库存和余额
static int stock = 100;
static int balance = 10000;
// 100个并发订单任务
static final int TASK_COUNT = 100;
static CountDownLatch latch = new CountDownLatch(TASK_COUNT);
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(20);
for (int i = 0; i < TASK_COUNT; i++) {
// 一半任务:先锁库存 → 再锁余额
if (i % 2 == 0) {
pool.submit(() -> {
try {
synchronized (STOCK_LOCK) {
System.out.println(Thread.currentThread().getName() + "拿到库存锁,等余额锁");
// 模拟业务耗时,放大死锁概率
Thread.sleep(100);
synchronized (BALANCE_LOCK) {
stock--;
balance -= 100;
System.out.println(Thread.currentThread().getName() + "扣减完成:库存=" + stock + ",余额=" + balance);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
} else {
// 另一半任务:先锁余额 → 再锁库存(锁顺序反了,必死锁)
pool.submit(() -> {
try {
synchronized (BALANCE_LOCK) {
System.out.println(Thread.currentThread().getName() + "拿到余额锁,等库存锁");
Thread.sleep(100);
synchronized (STOCK_LOCK) {
stock--;
balance -= 100;
System.out.println(Thread.currentThread().getName() + "扣减完成:库存=" + stock + ",余额=" + balance);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
}
latch.await();
pool.shutdown();
System.out.println("所有订单处理完成"); // 永远执行不到,死锁了!
}
}

运行结果:服务器卡死,CPU 飙到 100%
跑这段代码,控制台会停在 “拿到 XX 锁,等 XX 锁” 的日志,程序再也不往下走,CPU 直接拉满 —— 这就是死锁:多个线程拿着对方需要的锁,互相等待,谁也不放手,形成 “死循环”。
二、用 “抢杯子和笔”,讲透死锁的 4 个必要条件
“死锁其实特简单,就像你和同事抢东西,” 哇哥拿王二的水杯和自己的笔举例子,“你拿着杯子,想要我的笔;我拿着笔,想要你的杯子俩人为了等对方的东西,都不放手,就僵住了。死锁必须满足 4 个条件,少一个都锁不死:”

“只要打破其中一个条件,死锁就没了 —— 这是避免死锁的核心思路,比背 100 遍定义管用。”
三、3 招避免死锁,王二直接抄作业

🚀 第一招:固定锁的获取顺序(打破 “循环等待”)
最常用、最简单的方法:所有线程都按同一个顺序拿锁,比如 “先拿库存锁,再拿余额锁”,就算并发再高,也不会出现 “你等我、我等你” 的闭环。
修复代码(固定锁顺序)
package cn.tcmeta.deadlock;
import java.util.concurrent.*;
/**
* @author: laoren
* @description: 死锁修复版本
* @version: 1.0.0
*/
// 订单处理系统——已修复死锁
public class DeadLockSample {
// 库存锁:保护库存变量
static final Object STOCK_LOCK = new Object();
// 余额锁:保护余额变量
static final Object BALANCE_LOCK = new Object();
// 模拟库存和余额
static int stock = 100;
static int balance = 10000;
// 并发订单任务数
static final int TASK_COUNT = 100;
static CountDownLatch latch = new CountDownLatch(TASK_COUNT);
public static void main(String[] args) throws InterruptedException {
// 自定义线程池配置,避免使用默认Executors工厂类带来的隐患
ExecutorService pool = new ThreadPoolExecutor(
20,
20,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(TASK_COUNT),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < TASK_COUNT; i++) {
pool.submit(() -> {
try {
processOrder(); // 统一入口处理订单逻辑
} finally {
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("所有订单处理完成");
}
/**
* 处理单笔订单的核心逻辑
*/
private static void processOrder() {
try {
// 固定加锁顺序:始终先获取 STOCK_LOCK,再获取 BALANCE_LOCK,杜绝死锁
synchronized (STOCK_LOCK) {
System.out.println(Thread.currentThread().getName() + "拿到库存锁");
synchronized (BALANCE_LOCK) {
stock--;
balance -= 100;
System.out.println(Thread.currentThread().getName() + "扣减完成:库存=" + stock + ",余额=" + balance);
}
}
} catch (Exception e) {
Thread.currentThread().interrupt(); // 恢复中断标志位
e.printStackTrace();
}
}
}

哇哥划重点
“不管是订单、转账还是库存扣减,给锁定个‘优先级’(比如按锁对象的 hashCode 排序),所有线程都按这个顺序拿,循环等待的条件就没了,死锁根本发生不了。这招我用了 20 年,从没翻过车。”
📢 第二招:尝试加锁,超时放弃(打破 “持有并等待”)
用Lock的tryLock方法替代synchronized,拿锁超时就放弃,还释放已经拿到的锁 —— 不会 “拿着一个锁死等另一个锁”。
修复代码(tryLock 超时放弃)
package cn.tcmeta.deadlock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: laoren
* @description: // 修复方案2:尝试加锁+超时放弃
* @version: 1.0.0
*/
public class DeadLockFixByTryLock {
static Lock stockLock = new ReentrantLock();
static Lock balanceLock = new ReentrantLock();
static int stock = 100;
static int balance = 10000;
static final int TASK_COUNT = 100;
static CountDownLatch latch = new CountDownLatch(TASK_COUNT);
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(20);
for (int i = 0; i < TASK_COUNT; i++) {
pool.submit(() -> {
boolean stockLocked = false;
boolean balanceLocked = false;
try {
// 尝试拿库存锁,3秒超时
stockLocked = stockLock.tryLock(3, TimeUnit.SECONDS);
if (!stockLocked) {
System.out.println(Thread.currentThread().getName() + "拿库存锁超时,放弃执行");
return;
}
System.out.println(Thread.currentThread().getName() + "拿到库存锁,尝试拿余额锁");
// 尝试拿余额锁,3秒超时
balanceLocked = balanceLock.tryLock(3, TimeUnit.SECONDS);
if (!balanceLocked) {
System.out.println(Thread.currentThread().getName() + "拿余额锁超时,释放库存锁");
stockLock.unlock(); // 释放已拿到的锁,避免占着茅坑不拉屎
return;
}
// 扣减逻辑
stock--;
balance -= 100;
System.out.println(Thread.currentThread().getName() + "扣减完成:库存=" + stock + ",余额=" + balance);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放所有拿到的锁
if (balanceLocked) balanceLock.unlock();
if (stockLocked) stockLock.unlock();
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("所有订单处理完成!最终库存=" + stock + ",余额=" + balance);
}
}

王二拍腿叫好
“这招太适合支付场景了!就算拿锁失败,也能返回‘系统繁忙,请重试’,不会让用户一直等,体验比卡死强 10 倍!”
哇哥提醒
“记住:tryLock拿到锁后,一定要在finally里释放,不然就算没死锁,也会出现‘锁泄露’,慢慢把服务器堵死。”
✔️ 第三招:一次性获取所有锁(打破 “持有并等待”)

要么把需要的锁全拿到手,要么一个都不拿 —— 彻底避免 “拿着 A 锁等 B 锁” 的情况。
修复代码(一次性拿所有锁)
package cn.tcmeta.deadlock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: laoren
* @description: // 修复方案3:一次性获取所有锁
* @version: 1.0.0
*/
public class DeadLockFixByAllLock {
static Lock stockLock = new ReentrantLock();
static Lock balanceLock = new ReentrantLock();
static int stock = 100;
static int balance = 10000;
static final int TASK_COUNT = 100;
static CountDownLatch latch = new CountDownLatch(TASK_COUNT);
// 工具方法:一次性拿所有锁,拿不到就释放已拿的
private static boolean tryAcquireAllLocks(Lock... locks) throws InterruptedException {
for (Lock lock : locks) {
if (!lock.tryLock(3, TimeUnit.SECONDS)) {
for (Lock l : locks) {
if (l instanceof ReentrantLock && ((ReentrantLock) l).isHeldByCurrentThread()) {
l.unlock();
}
}
return false;
}
}
return true;
}
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(20);
for (int i = 0; i < TASK_COUNT; i++) {
pool.submit(() -> {
boolean allLocked = false;
try {
// 一次性拿库存锁+余额锁
allLocked = tryAcquireAllLocks(stockLock, balanceLock);
if (!allLocked) {
System.out.println(Thread.currentThread().getName() + "拿不到所有锁,放弃执行");
return;
}
// 扣减逻辑
stock--;
balance -= 100;
System.out.println(Thread.currentThread().getName() + "扣减完成:库存=" + stock + ",余额=" + balance);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放所有锁
if (allLocked) {
balanceLock.unlock();
stockLock.unlock();
}
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("所有订单处理完成!最终库存=" + stock + ",余额=" + balance);
}
}

适用场景
“适合需要多把锁的复杂业务,比如转账(扣 A 账户 + 加 B 账户)、跨表操作,一次性拿锁能从根源上避免持有并等待。”
四、线上死锁了?用 jstack 快速排查
“就算不小心写出死锁,也不用慌,Java 自带的 jstack 命令能 5 秒定位问题,” 哇哥教王二实操:
排查步骤(傻瓜式)
- 找进程 ID:打开终端,输入jps,找到死锁程序的 PID(比如1234 DeadLockDemo);
- 打印线程日志:输入jstack 1234,回车;
- 找死锁日志:日志里会标红Deadlock,还会告诉你哪个线程拿着哪个锁、等哪个锁,一目了然。
示例死锁日志(关键片段)
Found one Java-level deadlock:
=============================
Thread-1:
waiting to lock monitor 0x00007f9e12006800 (object 0x000000076ab82060, a java.lang.Object),
which is held by Thread-2
Thread-2:
waiting to lock monitor 0x00007f9e12008000 (object 0x000000076ab82070, a java.lang.Object),
which is held by Thread-1
“看,Thread-1 拿着余额锁等库存锁,Thread-2 拿着库存锁等余额锁,死锁实锤了!改下锁顺序就能解决。”
五、总结:死锁的 “防坑心法”
哇哥把核心要点缩成 3 句顺口溜,王二抄在便利贴贴显示器上:
- 死锁四条件,破一就完蛋:互斥、持有等待、不可剥夺、循环等待,打破任一就能防死锁;
- 锁序要固定,优先用这招:最简单、最稳的避坑方法,新手直接无脑用;
- 超时加放弃,复杂场景上:核心业务用 tryLock,避免卡死服务器;
- 排查用 jstack,线上不慌张:死锁了别重启,先 jstack 定位,再改代码。
哇哥的血泪彩蛋
“我刚工作时,写转账代码没固定锁顺序,把公司测试库搞死锁了,” 哇哥捂脸,“DBA 查了半天,最后重启数据库才恢复,我被罚写了 5000 字检讨 —— 从那以后,我写多锁代码必先定顺序,刻进 DNA 了!”
六、最后说句实在的

死锁看着吓人,其实就是 “线程互相卡脖子”,只要掌握 “打破必要条件” 的思路,就能轻松避免。今天这 3 套修复代码,你复制过去改改变量名就能用,不管是订单处理、转账系统还是库存扣减,都能避开死锁坑。
如果这篇救了你的服务器,点赞 + 分享给你那还在瞎写锁的同事!关注我,下次扒一扒 “活锁和饥饿”—— 比死锁更隐蔽的并发坑,让你面试时直接碾压面试官!



2万+

被折叠的 条评论
为什么被折叠?



