Java 多线程中的死锁(Deadlock),是两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的僵局。如果没有外力干涉,这些线程都无法继续执行。
下面从产生条件、代码示例、排查方法、预防策略几个方面来讲。
一、死锁产生的四个必要条件
死锁必须同时满足以下四个条件,缺一不可:
- 互斥条件:资源同一时刻只能被一个线程占用。
- 请求与保持条件:线程已经持有了至少一个资源,又去请求被其他线程占用的资源,且不释放自己已持有的资源。
- 不可剥夺条件:线程已获得的资源,在使用完之前不能被其他线程强行剥夺,只能自己释放。
- 循环等待条件:多个线程之间形成一种头尾相接的循环等待资源关系。
破坏其中任意一个条件,就能避免死锁。
二、经典代码示例
publicclassDeadLockDemo{privatestaticfinalObjectlockA=newObject();privatestaticfinalObjectlockB=newObject();publicstaticvoidmain(String[]args){Threadt1=newThread(()->{synchronized(lockA){System.out.println("线程1 获取 lockA");try{Thread.sleep(100);}catch(InterruptedExceptione){}synchronized(lockB){System.out.println("线程1 获取 lockB");}}});Threadt2=newThread(()->{synchronized(lockB){System.out.println("线程2 获取 lockB");try{Thread.sleep(100);}catch(InterruptedExceptione){}synchronized(lockA){System.out.println("线程2 获取 lockA");}}});t1.start();t2.start();}}- 线程1 持有 lockA,等待 lockB;
- 线程2 持有 lockB,等待 lockA;
- 两者互相等待,形成循环等待,程序卡死。
三、如何排查死锁
1. 使用jstack
jstack<pid>输出中会看到Found one Java-level deadlock:,并列出涉及死锁的线程和持有的锁。
2. 使用jconsole/VisualVM
图形化工具,连接进程后可以检测到死锁线程。
3. 使用ThreadMXBean编程检测
ThreadMXBeanmxBean=ManagementFactory.getThreadMXBean();long[]deadlockedThreads=mxBean.findDeadlockedThreads();if(deadlockedThreads!=null){ThreadInfo[]infos=mxBean.getThreadInfo(deadlockedThreads);for(ThreadInfoinfo:infos){System.out.println(info);}}四、预防与避免策略
1. 破坏循环等待条件(最常用)
统一加锁顺序:所有线程都按相同的顺序获取锁。
// 所有线程都先 lockA 再 lockBsynchronized(lockA){synchronized(lockB){...}}2. 破坏请求与保持条件
一次性申请所有资源,或者申请新资源前先释放已持有的资源。
3. 破坏不可剥夺条件
使用ReentrantLock.tryLock(long timeout, TimeUnit),超时拿不到锁就放弃并释放已持有的锁。
if(lockA.tryLock(1,TimeUnit.SECONDS)){try{if(lockB.tryLock(1,TimeUnit.SECONDS)){try{/* 业务逻辑 */}finally{lockB.unlock();}}}finally{lockA.unlock();}}4. 破坏互斥条件
一般很难破坏,因为很多资源本身就是互斥的。可以考虑用无锁结构(CAS、原子类)或读写锁来减少互斥范围。
5. 其他工程实践
- 减小锁粒度,缩短持锁时间;
- 避免锁嵌套;
- 使用
java.util.concurrent包中的并发工具类(如ConcurrentHashMap、Semaphore)替代手动加锁; - 使用带超时的锁,避免无限等待。
五、死锁 vs 活锁 vs 饥饿
| 概念 | 说明 |
|---|---|
| 死锁 | 线程互相等待,永远阻塞 |
| 活锁 | 线程不断重试、不断改变状态,但无法向前推进 |
| 饥饿 | 某线程长期得不到资源(如优先级太低) |
六、小结
- 死锁的本质是循环等待 + 不可剥夺;
- 排查靠
jstack、jconsole、ThreadMXBean; - 预防核心是统一加锁顺序和使用带超时的锁;
- 工程上更推荐用
JUC提供的高级并发工具,减少手动锁的使用。