文章目录
- 1、概念
- 2、解决和预防策略:
-
- [1. 按固定顺序获取锁(破坏"循环等待")](#1. 按固定顺序获取锁(破坏“循环等待”))
- [2. 使用超时机制(破坏"占有并等待")](#2. 使用超时机制(破坏“占有并等待”))
- [3. 一次性申请所有资源(破坏"占有并等待")](#3. 一次性申请所有资源(破坏“占有并等待”))
- [3、 死锁检测与恢复](#3、 死锁检测与恢复)
1、概念
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
由于"互斥"是锁的基本特性,我们无法破坏它。因此解决方案主要集中在破坏后三个条件。
2、解决和预防策略:
1. 按固定顺序获取锁(破坏"循环等待")
这是最常用、最有效的预防手段。强制所有线程以相同的顺序申请锁。
场景:
比如转账操作,线程 A 从账户 1 转给账户 2,线程 B 从账户 2 转给账户 1。如果一个先锁 1 再锁 2,另一个先锁 2 再锁 1,就容易死锁。
解决:规定一个全局顺序,比如永远先锁 ID 小的账户,再锁 ID 大的账户。
2. 使用超时机制(破坏"占有并等待")
不要让线程无限期地等待锁。如果在指定时间内拿不到锁,就放弃当前操作或稍后重试。
使用 ReentrantLock.tryLock():
java
1Lock lock1 = new ReentrantLock();
2Lock lock2 = new ReentrantLock();
3
4// 尝试获取锁,最多等待 1 秒
5if (lock1.tryLock(1, TimeUnit.SECONDS)) {
6 try {
7 if (lock2.tryLock(1, TimeUnit.SECONDS)) {
8 try {
9 // 执行业务逻辑
10 } finally {
11 lock2.unlock();
12 }
13 }
14 } finally {
15 lock1.unlock();
16 }
17}
18// 如果没拿到锁,可以记录日志、重试或返回失败,而不是死锁
使用 synchronized 的替代方案:synchronized 无法设置超时,一旦拿不到锁就会一直阻塞,所以高并发场景建议优先使用 ReentrantLock。
3. 一次性申请所有资源(破坏"占有并等待")
如果一个线程需要多个资源,要么全部申请到,要么一个都不申请。
实现:可以通过一个全局的"资源管理器"来统一分配资源,或者使用 tryLock 尝试获取所有需要的锁,如果有一个获取失败,就释放已获取的锁并重试。
缺点:实现起来比较复杂,可能会降低吞吐量。
3、 死锁检测与恢复
使用 JDK 工具:
- jstack:在命令行输入 jstack ,如果检测到死锁,JVM
会直接在控制台输出死锁信息,告诉你哪两个线程在互相等待哪个锁。 - jconsole / --jvisualvm:图形化界面工具,连接到 Java
进程后,点击"检测死锁"按钮,能直观地看到死锁的线程和堆栈。
| 策略 | 破坏的条件 | 推荐程度 | 说明 |
|---|---|---|---|
| 统一锁顺序 | 循环等待 | ⭐⭐⭐⭐⭐ | 最常用,通过排序强制顺序 |
| 超时获取锁 | 占有并等待 | ⭐⭐⭐⭐ | 使用 ReentrantLock.tryLock() |
| 死锁检测 | - | ⭐⭐⭐ | 发生后及时发现,利用 jstack 或 ThreadMXBean |
| 一次性申请 | 占有并等待 | ⭐⭐ | 实现复杂,性能可能不高 |