什么情况下Java程序会产生死锁?

本文详细解释了死锁的概念及发生原理,展示了如何通过jstack等工具定位死锁,并提供了几种预防死锁的有效方法。

典型回答

死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅会发生在线程之间,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。

就像下图中所示:

定位死锁最常见的方式就是利用jstack等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往jstack等就能直接定位,类似JConsole甚至可以在图形界面进行有限的死锁检测。

如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段互相审查,或者利用工具进行预防性排查,往往也是很重要的。

知识扩展

下面这个程序,只用了两个嵌套的synchronized去获取锁,几乎每次都可以重现死锁:

public class DeadLockSample extends Thread {
  private String first;
  private String second;
  public DeadLockSample(String name, String first, String second) {
    super(name);
    this.first = first;
    this.second = second;
  }

  public void run() {
    synchronized (first) {
      System.out.println(this.getName() + " obtained: " + first);
      try {
        Thread.sleep(1000L);
        synchronized (second) {
          System.out.println(this.getName() + " obtained: " + second);
        }
      } catch (InterruptedException e) {
        // Do Nothing
      }
    }
  }

  public static void main(String[] args) throws InterruptedException {
    String lockA = "lockA";
    String lockB = "lockB";
    DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB);
    DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
  }
}

某一次的输出如下:

Thread2 obtained: lockB
Thread1 obtained: lockA

这里有个比较有意思的地方,为什么先调用Thread1的start,但是Thread2却先打印出来了呢?这就是因为线程调度依赖于操作系统调度器。虽然你可以通过优先级之类进行影响,但是具体情况是不确定的。

下面来模拟问题定位,这里就选取最常见的jstack。首先,可以使用jps或者系统的ps命令、任务管理器等工具,确定进程ID。第二步,调用jstack获取线程栈:

>jstack your-pid

然后,分析得到的输出,具体片段如下:

Java stack information for the threads listed above:
===================================================
"Thread2":
        at cn.liximing.study.javacore.DeadLockSample.run(DeadLockSample.java:23)
        - waiting to lock <0x000000078097aea0> (a java.lang.String)
        - locked <0x000000078097aed8> (a java.lang.String)
"Thread1":
        at cn.liximing.study.javacore.DeadLockSample.run(DeadLockSample.java:23)
        - waiting to lock <0x000000078097aed8> (a java.lang.String)
        - locked <0x000000078097aea0> (a java.lang.String)

Found 1 deadlock.

上面的输出可以看出,jstack本身也会把类似的简单死锁抽取出来,直接打印出来。但是一些复杂的情况,则需要分析各个线程持有的锁,以及等待的锁进行分析。

如何在编程中尽量预防死锁?

首先,我们来总结一下前面例子中死锁的产生包含了哪些基本元素。基本上死锁的发生是因为:

  • 互斥条件,类似Java中Monitor都是独占的,要么是我用,要么是你用。
  • 互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其它线程抢占。
  • 循环依赖关系,两个或者多个个体之间出现了锁的链条环。

据此分析得到以下一些避免死锁的思路和方法。

方法一:如果可能的话,尽量避免使用多个锁,并且只有需要时才持有锁。

程序之所以需要持有多个锁,就是因为需要持有多个资源。从程序设计的角度反思,如果我们赋予一段程序太多的职责,出现“既要……又要……”的情况时,可能就需要我们审视下设计思路或目的是否合理了。

方法二:如果必须使用多个锁,尽量设计好锁的获取顺序。

这个说起来简单,做起来可不容易,你可以参考著名的“银行家算法”。一般的情况,建议可以采取一些简单的辅助手段。比如:将对象(方法)和锁之间的关系,用图形化的方式分别抽取出来。然后根据对象之间组合、调用的关系对比和组合,考虑可能调用的时序。按照可能时序合并,发现可能死锁的场景。

方法三:使用带超时的方法,为程序带来更多可控性。

类似Object.wait(...)或者CountDownLatch.await(...),都支持所谓的timed_wait,我们完全可以不假定该锁一定会获得,指定超时时间,并为无法得到锁时准备退出逻辑。

并发Lock实现,如ReentrantLock还支持非阻塞式的获取锁操作tryLock(),这是一个插队行为(barging),并不在乎等待的公平性。如果执行时对象恰好没有被独占,则直接获取锁。有时,我们希望条件允许就尝试插队,不然就按照现有公平性规则等待,一般采用下面的方法:

if (lock.tryLock() || lock.tryLock(timeout, unit)) {
  // ...
}

方法四:通过静态代码分析工具去查找固定模式。

业界也有一些其它方面的尝试,比如通过静态代码分析(如FindBugs)去查找固定的模式,进而定位可能的死锁或者竞争情况。实践证明这种方法也有一定的作用。

【完】

<think> 我们正在讨论Java中的死锁问题。用户明确询问导致死锁的情况及避免方法。 根据引用[3]:死锁产生有四个必要条件:互斥、持有并等待、不可剥夺、循环等待。 避免方法包括:固定锁顺序、减少锁持有时间、使用tryLock、使用并发工具类、死锁检测机制等。 同时引用[1]和[2]也提到了避免死锁的技术:加锁顺序、加锁时限和死锁检测。 此外,引用[4]提供了与其他知识点的关联,如synchronized、ReentrantLock、wait/notify、线程池等,这些在讨论死锁时都可能涉及。 因此,我们的回答应该包括: 1. 死锁产生的四个必要条件(引用[3]) 2. 常见的死锁场景(举例说明) 3. 避免死锁的方法(结合引用[1][2][3]) 4. 与其他并发知识点的联系(引用[4]) 注意:回答要结构清晰,尽量使用示例代码说明,并自然地添加引用标识。 </think> ### Java 中导致死锁的情况及避免方法 #### 一、导致死锁的四个必要条件(必须同时满足) 1. **互斥条件** 资源一次只能被一个线程占用(如 `synchronized` 锁) 2. **持有并等待** 线程持有资源时,同时请求其他资源(如锁定 A 后尝试锁定 B) 3. **不可剥夺** 资源只能由持有线程主动释放,不可强制抢占 4. **循环等待** 线程间形成闭环等待链(如 T1 持有 A 等 B,T2 持有 B 等 A)[^3] #### 二、常见死锁场景示例 ```java // 循环等待导致的死锁 public class DeadlockDemo { static final Object lockA = new Object(); static final Object lockB = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lockA) { // 持有 lockA try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (lockB) { // 尝试获取 lockB System.out.println("Thread1 done"); } } }).start(); new Thread(() -> { synchronized (lockB) { // 持有 lockB try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (lockA) { // 尝试获取 lockA System.out.println("Thread2 done"); } } }).start(); } } ``` **结果**:两个线程互相等待对方释放锁,程序永久阻塞[^3][^4]。 #### 三、避免死锁的核心方法 1. **固定锁顺序** 所有线程按全局统一顺序获取锁(如先锁 A 后锁 B) ```java // 修改后代码:统一先锁 lockA 再锁 lockB synchronized (lockA) { synchronized (lockB) { // 操作资源 } } ``` 2. **使用尝试锁(tryLock)** 通过 `ReentrantLock.tryLock()` 设置超时避免无限等待: ```java Lock lock1 = new ReentrantLock(); Lock lock2 = new ReentrantLock(); if (lock1.tryLock(1, TimeUnit.SECONDS)) { try { if (lock2.tryLock(1, TimeUnit.SECONDS)) { // 成功获取双锁 } } finally { lock2.unlock(); } } finally { lock1.unlock(); } ``` 3. **减少锁持有时间** 优化代码结构,只在必要操作时持有锁: ```java // 错误示例:耗时操作在锁内 synchronized(lock) { readFile(); // 文件读取耗时 updateData(); } // 正确做法:仅保护共享数据 Data temp = readFile(); // 锁外执行耗时操作 synchronized(lock) { updateData(temp); // 快速更新 } ``` 4. **死锁检测与恢复(高级)** - 使用 `ThreadMXBean` 检测死锁: ```java ThreadMXBean bean = ManagementFactory.getThreadMXBean(); long[] threadIds = bean.findDeadlockedThreads(); // 返回死锁线程ID ``` - 强制中断死锁线程(风险高,需谨慎)[^1] 5. **使用更高阶并发工具** 避免直接使用锁,改用线程安全容器: ```java // 替代 synchronized+HashMap ConcurrentMap<String, String> map = new ConcurrentHashMap<>(); map.put("key", "value"); // 内置锁分段技术 ``` #### 四、与其他并发机制的关系 1. **synchronized vs ReentrantLock** `synchronized` 自动释放锁,但无法中断; `ReentrantLock` 支持 `tryLock()` 和公平锁策略,更适合复杂场景[^4]。 2. **wait/notify 使用规范** 在循环中调用 `wait()`,避免虚假唤醒导致死锁: ```java synchronized(lock) { while (!condition) lock.wait(); // 正确用法 } ``` 3. **线程池配置** 避免任务依赖引发的线程饥饿死锁(如固定线程池中任务互相等待)[^4]。 > **经验法则**:通过静态代码分析工具(如 FindBugs)和压力测试(如 JMeter)可提前发现潜在死锁[^2]。并发编程应优先使用 `java.util.concurrent` 包中的高级工具(如 `Semaphore`, `CountDownLatch`),而非直接操作底层锁[^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值