Java死锁排查与预防:jstack与ReentrantLock.tryLock详解
一、死锁基础回顾
1.1 死锁四要素(Coffman条件)
死锁的发生必须同时满足以下四个条件:
- 互斥条件:资源只能被一个线程独占
- 请求与保持:线程持有资源A的同时请求资源B
- 不剥夺:已获得的资源不能被强制剥夺
- 循环等待:存在线程资源的环形等待链
// 经典死锁图示Thread-1:lockA.lock()→ 请求 lockBThread-2:lockB.lock()→ 请求 lockA ╭─────────────╮ │ 循环等待 │ ╰─────────────╯二、手写一个可复现的死锁
2.1 完整死锁示例代码
publicclassDeadlockDemo{// 两个共享资源privatestaticfinalObjectlockA=newObject();privatestaticfinalObjectlockB=newObject();publicstaticvoidmain(String[]args){// 线程1:先获取lockA,再请求lockBThreadt1=newThread(()->{synchronized(lockA){System.out.println("Thread-1: 持有 lockA");try{Thread.sleep(100);}catch(InterruptedExceptione){}// 确保线程2先拿到lockBSystem.out.println("Thread-1: 等待 lockB...");synchronized(lockB){System.out.println("Thread-1: 获取了 lockB");}}},"Thread-1");// 线程2:先获取lockB,再请求lockAThreadt2=newThread(()->{synchronized(lockB){System.out.println("Thread-2: 持有 lockB");try{Thread.sleep(100);}catch(InterruptedExceptione){}System.out.println("Thread-2: 等待 lockA...");synchronized(lockA){System.out.println("Thread-2: 获取了 lockA");}}},"Thread-2");t1.start();t2.start();// 等待线程执行(程序将永远卡死)try{t1.join();t2.join();}catch(InterruptedExceptione){e.printStackTrace();}}}运行这个程序,控制台输出:
Thread-1: 持有 lockA Thread-2: 持有 lockB Thread-1: 等待 lockB... Thread-2: 等待 lockA... [程序卡死]三、jstack死锁分析实战
3.1 jstack工具介绍
jstack是JDK自带的线程堆栈分析工具,可以快速定位死锁。
# 查找Java进程IDjps -l# 对目标进程执行jstackjstack<pid>>deadlock.log# 或实时查看jstack -l<pid>3.2 分析死锁日志
对上面的死锁程序执行jstack后,关键部分如下:
Found one Java-level deadlock: ============================= "Thread-2": waiting to lock monitor 0x0000000018f0e800 (object 0x00000000d6e0a000, a java.lang.Object), which is held by "Thread-1" "Thread-1": waiting to lock monitor 0x0000000018f0d800 (object 0x00000000d6e0a010, a java.lang.Object), which is held by "Thread-2" Java stack information for the threads listed above: =================================================== "Thread-2": at DeadlockDemo.lambda$main$1(DeadlockDemo.java:28) - waiting to lock <0x00000000d6e0a000> (a java.lang.Object) ← 等待lockA - locked <0x00000000d6e0a010> (a java.lang.Object) ← 已持有lockB at java.lang.Thread.run(Thread.java:748) "Thread-1": at DeadlockDemo.lambda$main$0(DeadlockDemo.java:15) - waiting to lock <0x00000000d6e0a010> (a java.lang.Object) ← 等待lockB - locked <0x00000000d6e0a000> (a java.lang.Object) ← 已持有lockA at java.lang.Thread.run(Thread.java:748) Found 1 deadlock.3.3 日志解读要点
- Found one Java-level deadlock: 明确检测到死锁
- Thread-2 waiting to lock… held by Thread-1: 清晰的等待关系
- 十六进制地址:
0x00000000d6e0a000对应代码中的lockA对象 - 线程栈: 精确到死锁发生的代码行号(DeadlockDemo.java:28)
定位技巧:
- locked:该线程已持有的锁- waiting to lock:该线程正在等待的锁- 通过对象地址匹配,画出死锁环
四、ReentrantLock.tryLock(timeout)避免死锁
4.1 tryLock机制原理
publicclassSafeLockDemo{privatestaticfinalReentrantLocklockA=newReentrantLock();privatestaticfinalReentrantLocklockB=newReentrantLock();publicstaticvoidmain(String[]args){// 线程1:尝试获取,失败则超时释放Threadt1=newThread(()->{try{if(lockA.tryLock(100,TimeUnit.MILLISECONDS)){try{System.out.println("Thread-1: 持有 lockA");Thread.sleep(50);System.out.println("Thread-1: 尝试获取 lockB...");if(lockB.tryLock(100,TimeUnit.MILLISECONDS)){try{System.out.println("Thread-1: 成功获取 lockB");}finally{lockB.unlock();}}else{System.out.println("Thread-1: 获取 lockB 超时,释放 lockA");}}finally{lockA.unlock();}}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}},"Thread-1");// 线程2:同逻辑Threadt2=newThread(()->{try{if(lockB.tryLock(100,TimeUnit.MILLISECONDS)){try{System.out.println("Thread-2: 持有 lockB");Thread.sleep(50);System.out.println("Thread-2: 尝试获取 lockA...");if(lockA.tryLock(100,TimeUnit.MILLISECONDS)){try{System.out.println("Thread-2: 成功获取 lockA");}finally{lockA.unlock();}}else{System.out.println("Thread-2: 获取 lockA 超时,释放 lockB");}}finally{lockB.unlock();}}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}},"Thread-2");t1.start();t2.start();}}运行输出(不死锁):
Thread-1: 持有 lockA Thread-2: 持有 lockB Thread-1: 尝试获取 lockB... Thread-2: 尝试获取 lockA... Thread-1: 获取 lockB 超时,释放 lockA Thread-2: 获取 lockA 超时,释放 lockB [程序正常结束]4.2 tryLock核心优势
打破死锁的"不剥夺"和"循环等待"条件:
- 超时机制:不再无限等待,主动放弃
- 响应中断:可被
interrupt()唤醒 - 公平性选择:支持公平锁,减少饥饿
// 带公平锁的创建方式privatestaticfinalReentrantLockfairLockA=newReentrantLock(true);4.3 最佳实践模式
模式1:限时获取多锁
publicbooleantransfer(Accountfrom,Accountto,doubleamount,longtimeout){longstopTime=System.nanoTime()+timeout;while(System.nanoTime()<stopTime){if(from.lock.tryLock()){try{if(to.lock.tryLock()){try{if(from.balance>=amount){from.balance-=amount;to.balance+=amount;returntrue;}}finally{to.lock.unlock();}}}finally{from.lock.unlock();}}// 退避策略,避免活锁Thread.yield();}returnfalse;// 超时失败}模式2:固定顺序获取锁
// 通过对象ID排序,确保所有线程按相同顺序获取锁privatevoidsafeLock(Accounta1,Accounta2){if(a1.getId()<a2.getId()){synchronized(a1){synchronized(a2){/* 操作 */}}}else{synchronized(a2){synchronized(a1){/* 操作 */}}}}五、其他死锁避免策略
5.1 使用并发工具类
ConcurrentHashMap替代手动锁Map:
// ❌ 手动加锁,易死锁synchronized(map){map.put(key,value);}// ✅ 无锁化操作map.putIfAbsent(key,value);5.2 死锁检测工具
Arthas在线诊断(比jstack更强大):
# 安装Arthascurl-O https://arthas.aliyun.com/arthas-boot.jar# 启动并attach到进程java -jar arthas-boot.jar# 一键检测死锁[arthas]$ thread -b输出示例:
"thread-2" Id=13 BLOCKED on java.lang.Object@7d4991ad owned by "thread-1" Id=12 "thread-1" Id=12 BLOCKED on java.lang.Object@16b4a017 owned by "thread-2" Id=13 [arthas]$ thread 12 # 查看线程12详细堆栈5.3 代码规范预防
避免嵌套锁:
// ❌ 高风险publicvoidmethodA(){synchronized(lock1){methodB();// 内部可能获取lock2}}// ✅ 重构为无锁publicvoidmethodA(){// 预先获取所有需要的数据Datadata=copyData();methodB(data);// 无锁操作}锁粒度最小化:
// ❌ 粗粒度锁synchronized(bigLock){// 大量操作}// ✅ 细粒度锁Objectdata=getData();synchronized(dataLock){update(data);// 只保护关键部分}saveToDB(data);六、总结:死锁排查与预防清单
6.1 死锁排查三步法
- 现象确认:应用卡死、CPU使用率不高、线程状态BLOCKED
- jstack定位:
jstack pid > log.txt,搜索"deadlock" - 日志分析:识别
waiting to lock和locked的循环关系
6.2 死锁预防四原则
| 原则 | 实践方案 | 工具/代码示例 |
|---|---|---|
| 互斥→共享 | 使用并发容器 | ConcurrentHashMap |
| 请求保持→一次性 | 批量获取锁 | tryLock(timeout) |
| 不剥夺→可中断 | 锁可被超时释放 | ReentrantLock.tryLock() |
| 循环等待→顺序化 | 按固定顺序加锁 | if (id1 < id2) { lock1.lock(); lock2.lock(); } |
6.3 选型决策树
是否需要锁? ├─ 否 → 无锁设计(CAS、并发容器) └─ 是 → 是否需要重入? ├─ 否 → synchronized └─ 是 → 是否需要超时/中断? ├─ 否 → ReentrantLock └─ 是 → tryLock(timeout) ⭐金句:死锁是并发编程的"癌症",预防胜于治疗。通过tryLock打破无限等待、通过jstack快速定位、通过并发工具减少锁依赖,三管齐下才能构建健壮的并发系统。