在多线程并发编程中死锁Deadlock是一个非常经典且棘手的问题。继之前的线程安全总结之后本文聚焦于“死锁”总结它的产生条件、代码表现、排查方式以及预防策略。一、什么是死锁死锁指的是两个或多个线程在执行过程中因争夺资源而造成的一种互相等待的现象若无外力干涉它们都将无法推进下去。简单来说就是线程 A 持有锁 1等待锁 2线程 B 持有锁 2等待锁 1两者都在等待对方释放锁结果谁也拿不到程序永久卡死。二、死锁产生的四个必要条件死锁的发生必须同时满足以下四个条件Coffman 条件只要破坏其中任意一个死锁就无法形成互斥条件资源同一时间只能被一个线程占用。例如synchronized锁、ReentrantLock锁。请求与保持条件持有并等待线程已经持有了至少一个资源但又提出了新的资源请求而该资源被其他线程持有。线程此时不释放已持有的资源而是处于等待状态。不可剥夺条件不可抢占线程已获得的资源在未使用完之前不能被其他线程强行剥夺。只能由持有线程主动释放。循环等待条件存在一个线程与资源的循环等待链。例如A 等待 B 持有的资源B 等待 A 持有的资源。三、死锁代码示例下面是一个最经典的死锁演示publicclassDeadlockDemo{privatestaticfinalObjectlockAnewObject();privatestaticfinalObjectlockBnewObject();publicstaticvoidmain(String[]args){Threadt1newThread(()-{synchronized(lockA){System.out.println(Thread.currentThread().getName() 获取到 lockA);try{Thread.sleep(1000);}catch(InterruptedExceptione){}System.out.println(Thread.currentThread().getName() 等待 lockB...);synchronized(lockB){System.out.println(Thread.currentThread().getName() 获取到 lockB);}}},线程A);Threadt2newThread(()-{synchronized(lockB){System.out.println(Thread.currentThread().getName() 获取到 lockB);try{Thread.sleep(1000);}catch(InterruptedExceptione){}System.out.println(Thread.currentThread().getName() 等待 lockA...);synchronized(lockA){System.out.println(Thread.currentThread().getName() 获取到 lockA);}}},线程B);t1.start();t2.start();}}运行结果程序会卡住两个线程互相等待永远不会结束。四、如何排查死锁1. 使用 jstack 命令jstack是 JDK 自带的线程堆栈分析工具可以快速发现死锁。# 1. 找到 Java 进程 PIDjps# 2. 输出线程堆栈jstackpid如果发生死锁jstack 输出末尾会明确提示Found one Java-level deadlock: 线程B: waiting to lock monitor 0x00007f... (object 0x000000076ab..., a java.lang.Object), which is held by 线程A 线程A: waiting to lock monitor 0x00007f... (object 0x000000076ac..., a java.lang.Object), which is held by 线程B2. 使用 jconsole / VisualVM打开jconsole连接到进程切换到“线程”选项卡点击“检测死锁”即可可视化查看死锁线程。3. 使用 ThreadMXBean 代码检测可以在代码中主动检测死锁importjava.lang.management.ManagementFactory;importjava.lang.management.ThreadMXBean;publicclassDeadlockDetector{publicstaticvoidmain(String[]args){ThreadMXBeanmxBeanManagementFactory.getThreadMXBean();long[]deadlockedThreadsmxBean.findDeadlockedThreads();if(deadlockedThreads!null){System.out.println(检测到死锁线程数deadlockedThreads.length);}else{System.out.println(未检测到死锁);}}}五、如何预防和避免死锁1. 破坏循环等待条件固定加锁顺序所有线程都按照相同的顺序获取锁这是最常见、最简单的预防方式。// 统一按照 lockA - lockB 的顺序加锁synchronized(lockA){synchronized(lockB){// ...}}2. 破坏请求与保持条件一次性申请所有资源要么一次性获取所有需要的锁要么一个都不获取。// 使用一个全局锁来保护多个资源的申请synchronized(globalLock){// 获取所有需要的资源}3. 破坏不可剥夺条件使用 tryLock 超时使用ReentrantLock.tryLock()设置超时时间获取不到锁就放弃避免无限等待。ReentrantLocklock1newReentrantLock();ReentrantLocklock2newReentrantLock();publicvoidmethod(){try{if(lock1.tryLock(1,TimeUnit.SECONDS)){try{if(lock2.tryLock(1,TimeUnit.SECONDS)){try{// 执行业务}finally{lock2.unlock();}}}finally{lock1.unlock();}}}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}4. 减少锁的使用范围与粒度尽量缩小同步代码块的范围避免在持有锁时调用外部方法避免嵌套锁。5. 使用无锁数据结构使用AtomicInteger、ConcurrentHashMap等并发工具从根本上减少锁的使用。6. 使用带超时机制的锁尽量使用tryLock(timeout)而不是无条件阻塞的lock()。六、死锁、活锁与饥饿的区别概念说明死锁线程互相等待对方释放资源永远阻塞无法推进活锁线程没有被阻塞但不断重试、回退始终无法取得进展如互相礼让饥饿某些线程长期得不到资源或 CPU 调度导致一直无法执行死锁你等我我等你大家都卡住。活锁你让我我让你结果谁都没走。饥饿低优先级线程永远抢不到锁或 CPU 时间。七、常见死锁场景总结场景说明加锁顺序不一致线程 A 先锁 1 再锁 2线程 B 先锁 2 再锁 1嵌套锁持有锁 A 时请求锁 B持有锁 B 时请求锁 A数据库事务两个事务互相等待对方持有的行锁线程池与任务依赖任务 A 等待任务 B 的结果而任务 B 又在等待任务 A资源分配多个线程竞争多个不可抢占的资源八、总结死锁是多线程编程中必须警惕的问题。它的产生需要同时满足四个条件互斥请求与保持不可剥夺循环等待预防死锁的核心思路就是破坏其中任意一个条件固定加锁顺序破坏循环等待一次性申请所有资源破坏请求与保持使用 tryLock 超时破坏不可剥夺减少锁的使用和范围降低发生概率排查死锁的常用工具jstackjconsoleVisualVMThreadMXBean实际开发中应尽量能不共享就不共享能无锁就无锁必须加锁就统一顺序、缩小范围、设置超时。只有这样才能写出既高效又安全的多线程程序。