☰
Java 死锁总结:原因、排查与预防
2026/9/26 6:03:23 网站建设 项目流程

在多线程并发编程中,死锁(Deadlock)是一个非常经典且棘手的问题。继之前的线程安全总结之后,本文聚焦于“死锁”,总结它的产生条件、代码表现、排查方式以及预防策略。


一、什么是死锁?

死锁指的是:

两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,它们都将无法推进下去。

简单来说,就是:

  • 线程 A 持有锁 1,等待锁 2;
  • 线程 B 持有锁 2,等待锁 1;
  • 两者都在等待对方释放锁,结果谁也拿不到,程序永久卡死。

二、死锁产生的四个必要条件

死锁的发生必须同时满足以下四个条件(Coffman 条件),只要破坏其中任意一个,死锁就无法形成:

  1. 互斥条件

    • 资源同一时间只能被一个线程占用。
    • 例如:synchronized锁、ReentrantLock锁。
  2. 请求与保持条件(持有并等待)

    • 线程已经持有了至少一个资源,但又提出了新的资源请求,而该资源被其他线程持有。
    • 线程此时不释放已持有的资源,而是处于等待状态。
  3. 不可剥夺条件(不可抢占)

    • 线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺。
    • 只能由持有线程主动释放。
  4. 循环等待条件

    • 存在一个线程与资源的循环等待链。
    • 例如:A 等待 B 持有的资源,B 等待 A 持有的资源。

三、死锁代码示例

下面是一个最经典的死锁演示:

publicclassDeadlockDemo{privatestaticfinalObjectlockA=newObject();privatestaticfinalObjectlockB=newObject();publicstaticvoidmain(String[]args){Threadt1=newThread(()->{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");Threadt2=newThread(()->{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. 输出线程堆栈jstack<pid>

如果发生死锁,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 "线程B"

2. 使用 jconsole / VisualVM

  • 打开jconsole,连接到进程;
  • 切换到“线程”选项卡;
  • 点击“检测死锁”,即可可视化查看死锁线程。

3. 使用 ThreadMXBean 代码检测

可以在代码中主动检测死锁:

importjava.lang.management.ManagementFactory;importjava.lang.management.ThreadMXBean;publicclassDeadlockDetector{publicstaticvoidmain(String[]args){ThreadMXBeanmxBean=ManagementFactory.getThreadMXBean();long[]deadlockedThreads=mxBean.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()设置超时时间,获取不到锁就放弃,避免无限等待。

ReentrantLocklock1=newReentrantLock();ReentrantLocklock2=newReentrantLock();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
资源分配多个线程竞争多个不可抢占的资源

八、总结

死锁是多线程编程中必须警惕的问题。它的产生需要同时满足四个条件:

  1. 互斥
  2. 请求与保持
  3. 不可剥夺
  4. 循环等待

预防死锁的核心思路就是破坏其中任意一个条件:

  • 固定加锁顺序(破坏循环等待)
  • 一次性申请所有资源(破坏请求与保持)
  • 使用 tryLock 超时(破坏不可剥夺)
  • 减少锁的使用和范围(降低发生概率)

排查死锁的常用工具:

  • jstack
  • jconsole
  • VisualVM
  • ThreadMXBean

实际开发中,应尽量:

能不共享就不共享,能无锁就无锁,必须加锁就统一顺序、缩小范围、设置超时。

只有这样,才能写出既高效又安全的多线程程序。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询