哈喽,大家好呀,我是呼噜噜,好久没有更新了,在现代计算机中,为了极致的压榨多核CPU的性能,提高程序的响应速度和处理效率,多线程是一种必不可少的优化手段。但是当我们在享受多线程带来性能提升的同时,也面临了一系列复杂的问题,其中最著名也最棘手就是死锁Deadlock问题
本文带你深入理解死锁,并通过实际案例解析,学会如何在实际开发中避免它
什么是多线程?
首先我们得明白进程和线程的概念,在计算机的世界中,进程Process是操作系统中资源分配的基本单位,进程之间的资源是独立隔离的,能很好的进行资源管理和保护。
而线程(Thread)则是进程中的一个执行任务(控制单元),负责当前进程中程序的执行。线程是资源调度的基本单位,我们可以将一个进程比作一个厨房,那么线程就是厨房里的厨师。一个进程可以包含多个线程,至少包含一个线程,与进程不同的是多个线程之间资源数据是共享的
多线程的优势:
- CPU单核时代,多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率,当一个线程被 IO 阻塞,其他线程还可以继续使用 CPU
- CPU多核时代,多线程主要是为了提高进程利用多核 CPU 的能力,多个线程可以被映射到底层多个 CPU核心 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高。

多线程之间的资源共享
在多线程编程中,线程之间的协作和资源共享是常见的需求。虽然多线程可以协作,但当多个线程需要同时访问或修改同一个共享资源(比如一个共享变量、一个文件)时,问题就来了。这就像厨房里只有一口锅,两个厨师都想用它来炒菜,如果不加协调,场面就会一片混乱,菜也炒不好。
这个时候,我们往往会引入锁(Lock)来保护共享资源,它也叫互斥锁 Mutex,它能确保在任何时刻,只有一个线程可以访问被保护的资源
当一个线程需要访问资源时,它必须先获取锁。如果锁已经被其他线程持有,那么该线程就会被阻塞(进入等待状态),直到持有锁的线程释放锁。
我们来看一个例子(java版):
// 模拟一个电商购物系统
class ECommerce {
private double totalAmount; // 共享资源:订单总金额
private int stock; // 共享资源:商品库存
// 构造函数,初始化库存和金额
public ECommerce(int initialStock) {
this.totalAmount = 0.0;
this.stock = initialStock;
}
// 下单方法(线程安全)
// 每次下单会减少库存,并累加订单金额
public synchronized void placeOrder(int quantity, double pricePerItem) {
System.out.println(Thread.currentThread().getName() + " 尝试下单: 数量=" + quantity + " 单价=" + pricePerItem);
// 判断库存是否足够
if (stock >= quantity) {
// 扣减库存
stock -= quantity;
// 累加订单总金额
double orderAmount = quantity * pricePerItem;
totalAmount += orderAmount;
System.out.println(Thread.currentThread().getName() + " 下单成功,总金额增加 " + orderAmount
+ ",剩余库存=" + stock + ",累计金额=" + totalAmount);
} else {
System.out.println(Thread.currentThread().getName() + " 下单失败,库存不足!");
}
}
// 获取当前总金额
public synchronized double getTotalAmount() {
return totalAmount;
}
// 获取剩余库存
public synchronized int getStock() {
return stock;
}
}
public class ECommerceExample {
public static void main(String[] args) throws InterruptedException {
// 假设系统初始有 10 件商品
ECommerce shop = new ECommerce(10);
// 模拟 3 个用户同时下单
Thread user1 = new Thread(() -> shop.placeOrder(3, 100.0), "用户A");
Thread user2 = new Thread(() -> shop.placeOrder(5, 100.0), "用户B");
Thread user3 = new Thread(() -> shop.placeOrder(4, 100.0), "用户C");
// 启动线程
user1.start();
user2.start();
user3.start();
// 等待所有线程完成
user1.join();
user2.join();
user3.join();
// 打印最终统计
System.out.println("操作结束,最终库存=" + shop.getStock() + ",最终金额=" + shop.getTotalAmount());
}
}执行结果:
用户A 尝试下单: 数量=3 单价=100.0
用户A 下单成功,总金额增加 300.0,剩余库存=7,累计金额=300.0
用户B 尝试下单: 数量=5 单价=100.0
用户B 下单成功,总金额增加 500.0,剩余库存=2,累计金额=800.0
用户C 尝试下单: 数量=4 单价=100.0
用户C 下单失败,库存不足!
操作结束,最终库存=2,最终金额=800.0什么是死锁?
凡是有利有弊,如果使用锁不当, 则会引发一个更严重的问题——死锁,它会让程序进入无尽的等待。死锁是指,两个或多个线程相互等待对方释放资源,从而导致程序无法继续执行的情况。
比如,线程 A 持有共享资源 X,等待获取共享资源 Y;而线程 B 持有共享资源 Y,等待获取共享资源 X。 此时这两个线程就陷入了死锁状态,无法继续执行。若无外力干涉,它们都将无法推进下去

趁热打铁,我们接着来看一个典型的死锁案例(Java版):
public class LockOrderDeadlock {
// 创建两个对象作为锁资源
private static final Object lockX = new Object();
private static final Object lockY = new Object();
public static void main(String[] args) {
// 创建线程 A
Thread threadA = new Thread(() -> {
// 线程 A 的执行逻辑
try {
// 首先,获取 lockX 的锁
synchronized (lockX) {
System.out.println(Thread.currentThread().getName() + " 获得了 lockX 锁");
// 暂停 1 秒,确保此时线程 B 有机会启动并获取 lockX
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 尝试获取 lockY 锁...");
// 尝试获取 lockB 的锁
synchronized (lockY) {
System.out.println(Thread.currentThread().getName() + " 获得了 lockY 锁");
}
}
} catch (InterruptedException e) {
// 处理线程中断异常
e.printStackTrace();
}
}, "线程 A");
// 创建线程 B
Thread threadB = new Thread(() -> {
// 线程 B 的执行逻辑
try {
// 首先,获取 lockB 的锁
synchronized (lockY) {
System.out.println(Thread.currentThread().getName() + " 获得了 lockY 锁");
// 暂停 1 秒,确保线程 A 能保持持有 lockX
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 尝试获取 lockY 锁...");
// 尝试获取 lockY 的锁
synchronized (lockX) {
System.out.println(Thread.currentThread().getName() + " 获得了 lockY 锁");
}
}
} catch (InterruptedException e) {
// 处理线程中断异常
e.printStackTrace();
}
}, "线程 B");
// 启动两个线程
threadA.start();
threadB.start();
}
}执行结果:
线程 A 获得了 lockX 锁
线程 B 获得了 lockY 锁
线程 B 尝试获取 lockY 锁...
线程 A 尝试获取 lockY 锁...由此我们可以总结出,死锁的四个必要条件:
- 互斥条件(Mutual Exclusion):一个资源每次只能被一个线程使用。这是锁的基本特性,无法破坏。
- 请求与保持条件(Hold and Wait):一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件(No Preemption):线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺,只能在使用完后由自己释放。
- 循环等待条件(Circular Wait):若干线程之间形成一种头尾相接的循环等待资源关系。例如,线程A等待线程B的资源,线程B又在等待线程A的资源。
一个死锁的发生,必须同时满足上述四个条件。也就是说只要我们,破坏其中任意一个,就能避免死锁

如何避免死锁
- **按固定顺序获取锁 **
按固定顺序获取锁,这样可以破坏循环等待条件 ,确保所有线程都以相同的顺序来请求锁,这是我们在实际开发中,最常用也是最有效的避免死锁的方法。
比如上面死锁的例子,我们调整2个线程获取锁的顺序一致,都是必须先获取 lockX,再获取 lockY。
public class FixedLockOrder {
// 创建两个对象作为锁资源
private static final Object lockX = new Object();
private static final Object lockY = new Object();
public static void main(String[] args) {
// 创建线程 A
Thread threadA = new Thread(() -> {
// 线程 A 的执行逻辑
try {
// 首先,获取 lockX 的锁
synchronized (lockX) {
System.out.println(Thread.currentThread().getName() + " 获得了 lockX 锁");
// 暂停 1 秒,模拟一些耗时操作
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 尝试获取 lockY 锁...");
// 尝试获取 lockY 的锁
synchronized (lockY) {
System.out.println(Thread.currentThread().getName() + " 获得了 lockY 锁");
}
}
} catch (InterruptedException e) {
// 处理线程中断异常
e.printStackTrace();
}
}, "线程 A");
// 创建线程 B
Thread threadB = new Thread(() -> {
// 线程 B 的执行逻辑
try {
// *** 修改点:同样先获取 lockX 的锁,保持与线程 A 一致的顺序 ***
synchronized (lockX) {
System.out.println(Thread.currentThread().getName() + " 获得了 lockX 锁");
// 暂停 1 秒,模拟一些耗时操作
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 尝试获取 lockY 锁...");
// 接着尝试获取 lockY 的锁
synchronized (lockY) {
System.out.println(Thread.currentThread().getName() + " 获得了 lockY 锁");
}
}
} catch (InterruptedException e) {
// 处理线程中断异常
e.printStackTrace();
}
}, "线程 B");
// 启动两个线程
threadA.start();
threadB.start();
}
}输出结果:
线程 A 获得了 lockX 锁
线程 A 尝试获取 lockY 锁...
线程 A 获得了 lockY 锁
线程 B 获得了 lockX 锁
线程 B 尝试获取 lockY 锁...
线程 B 获得了 lockY 锁这样线程A和线程B都能先后获取到锁lockX、lockY,没有发生死锁
- 一次性获取所有锁
一次性获取所有锁,这样就破坏请求与保持条件,不持有任何锁的同时,一次性地尝试获取所有需要的锁。如果能全部获取成功,则执行;如果有一个获取失败,则释放所有已经获取到的锁,然后等待一段时间再重试。这通常需要使用 java.util.concurrent.locks.Lock 接口的 tryLock() 方法来实现
我们来看一个例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AvoidDeadlockByTryLock {
// 使用 ReentrantLock 替代 synchronized
private static final Lock lockX = new ReentrantLock();
private static final Lock lockY = new ReentrantLock();
public static void main(String[] args) {
// 线程 A:先尝试获取 lockX,再尝试获取 lockY
Thread threadA = new Thread(() -> doWork(lockX, lockY), "线程 A");
// 线程 B:先尝试获取 lockY,再尝试获取 lockX
Thread threadB = new Thread(() -> doWork(lockY, lockX), "线程 B");
threadA.start();
threadB.start();
}
/**
* 通用的任务执行逻辑,避免重复代码
*/
private static void doWork(Lock firstLock, Lock secondLock) {
boolean gotFirst = false;
boolean gotSecond = false;
try {
while (true) {
// 尝试获取第一个锁
gotFirst = firstLock.tryLock();
if (gotFirst) {
System.out.println(Thread.currentThread().getName() + " 成功获取第一个锁");
// 尝试获取第二个锁
gotSecond = secondLock.tryLock();
if (gotSecond) {
System.out.println(Thread.currentThread().getName() + " 成功获取第二个锁");
break; // 两个锁都获取成功,退出循环
} else {
System.out.println(Thread.currentThread().getName() + " 获取第二个锁失败,释放第一个锁");
firstLock.unlock();
}
}
// 避免活锁:随机等待一小段时间再重试
Thread.sleep((long) (Math.random() * 10));
}
// 在这里执行需要两个锁保护的业务逻辑
System.out.println(Thread.currentThread().getName() + " 执行业务逻辑...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 确保释放已持有的锁
if (gotSecond) {
secondLock.unlock();
}
if (gotFirst) {
firstLock.unlock();
}
}
}
}这样是没有死锁了,但它实现复杂度高,如果需要同时获取的锁数量很多,那我们就必须写一个循环去逐个 tryLock(),一旦有一个失败,就要释放已经获得的锁。另外需要额外的代码来保证“部分成功时回滚”(释放已拿到的锁),否则容易导致锁泄漏。
另外是性能损耗大、容易出现活锁、可扩展性差
活锁是指线程并未阻塞,它们都在持续运行,但却无法取得任何进展。它们不断地改变自身状态以响应其他线程的状态变化,但最终导致谁也无法完成任务。

- 使用带超时的锁
使用带超时的锁,这样破坏不可剥夺条件,线程在尝试获取锁时,如果超过一定时间仍未获取到,则放弃本次请求,并释放自己,已经获取的所有锁,然后重试或执行其他逻辑。这是一个非常经典的兜底策略
public class AvoidDeadlockByTimeoutLockUpdated {
// 创建两个 ReentrantLock 实例作为锁资源
private static final ReentrantLock lockX = new ReentrantLock();
private static final ReentrantLock lockY = new ReentrantLock();
public static void main(String[] args) {
// 创建并启动线程 A
Thread threadA = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 启动,准备获取锁...");
// 1. 尝试在 2 秒内获取 lockX
if (lockX.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 成功获取了 lockX 锁");
// 为了让死锁场景更容易复现,在这里暂停 500 毫秒
// 这给了线程 B 足够的时间去获取 lockY
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " 准备获取 lockY 锁...");
// 2. 尝试在 2 秒内获取 lockY
if (lockY.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 成功获取了 lockY 锁");
System.out.println(Thread.currentThread().getName() + " ===> 成功完成任务!");
} else {
System.out.println(Thread.currentThread().getName() + " 获取 lockY 锁超时,任务失败。");
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取 lockX 锁超时,任务失败。");
}
} catch (InterruptedException e) {
// 线程在 sleep 或 tryLock 期间被中断时会抛出此异常
System.err.println(Thread.currentThread().getName() + " 被中断了。");
Thread.currentThread().interrupt(); // 重新设置中断状态
} finally {
// 3. 使用 finally 块确保锁一定会被释放
// 在释放前,必须检查当前线程是否真的持有了该锁,否则会抛出 IllegalMonitorStateException
if (lockY.isHeldByCurrentThread()) {
lockY.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了 lockY 锁。");
}
if (lockX.isHeldByCurrentThread()) {
lockX.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了 lockX 锁。");
}
}
}, "线程 A");
// 创建并启动线程 B (与线程 A 的加锁顺序相反)
Thread threadB = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 启动,准备获取锁...");
// 1. 尝试在 2 秒内获取 lockY
if (lockY.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 成功获取了 lockY 锁");
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " 准备获取 lockX 锁...");
// 2. 尝试在 2 秒内获取 lockX
if (lockX.tryLock(2, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 成功获取了 lockX 锁");
System.out.println(Thread.currentThread().getName() + " ===> 成功完成任务!");
} else {
System.out.println(Thread.currentThread().getName() + " 获取 lockX 锁超时,任务失败。");
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取 lockY 锁超时,任务失败。");
}
} catch (InterruptedException e) {
System.err.println(Thread.currentThread().getName() + " 被中断了。");
Thread.currentThread().interrupt();
} finally {
// 3. 确保锁的释放
if (lockX.isHeldByCurrentThread()) {
lockX.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了 lockX 锁。");
}
if (lockY.isHeldByCurrentThread()) {
lockY.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了 lockY 锁。");
}
}
}, "线程 B");
// 启动两个线程
threadA.start();
threadB.start();
}
}输出结果:
线程 A 启动,准备获取锁...
线程 B 启动,准备获取锁...
线程 A 成功获取了 lockX 锁
线程 B 成功获取了 lockY 锁
线程 B 准备获取 lockX 锁...
线程 A 准备获取 lockY 锁...
线程 A 获取 lockY 锁超时,任务失败。
线程 B 获取 lockX 锁超时,任务失败。
线程 B 释放了 lockY 锁。
线程 A 释放了 lockX 锁。我们可以发现由于过期时间的存在,哪怕无法获取锁,也不会出现死锁
死锁的排查
当我们的程序出现死锁时,我们该如何排查呢?

最常见的就是使用 jdk 自带的线程堆栈分析工具jstack,使用它需要先通过jps -l找到当前正在运行的Java进程的PID。然后执行jstack -l <PID>,来打印指定进程的线程堆栈信息

如果发生死锁,jstack 的输出结果会在末尾明确地提示 Found one deadlock,并详细列出死锁的线程、它们正在等待的锁以及它们已经持有的锁。
另一个就是现在比较火的监控工具,Arthas,这是是阿里开源的Java诊断工具,它直接提供了专门的命令来检测死锁thread -b

尾语
多线程确实能极大的提升我们程序的性能,但它也是把双刃剑,掌握好锁机制,是驾驭它的关键。死锁其实并没有我们想那么可怕。只需牢记,死锁只有同时满足互斥、持有并等待、不可剥夺、循环等待这四个必要条件的时候才会发生,我们只需破坏其中一个,即可避免死锁。最后别忘了利用诊断工具排查具体的线程死锁问题
点赞收藏在看就是对笔者最好的催更!最后如果大家对死锁有自己的见解,欢迎在下方留言,我们下期再见~
参考资料:
深入理解与避免Java 死锁_java中 什么是死锁 怎么避免死锁

作者:小牛呼噜噜
本文到这里就结束啦,感谢阅读,关注同名公众号:小牛呼噜噜,防失联+获取更多技术干货