腾讯面试题:什么情况下Java程序会产生死锁?如何定位、修复?

文摘   2025-01-03 12:05   陕西  

今天我们来聊聊Java程序中的死锁问题,看看它是怎么发生的,又该如何避免。其实,死锁是个非常棘手的问题,尤其在多线程编程中,想要在没有深入理解的情况下完全避免死锁,几乎是不可能的。

所以今天我就用一个简单的例子来为大家解开这个难题,并分享一下如何定位和修复死锁。

死锁是什么?它为什么会发生?

首先,我们要搞清楚死锁的定义。死锁通常出现在多线程环境中,它指的是两个或多个线程互相等待对方释放锁,从而导致程序停滞不前,所有相关线程永远无法继续执行。

这就像是一群小孩在玩"石头剪刀布",大家都在等别人出手,但谁也不愿意先动。最终大家就这样站着不动。

死锁的典型特征可以总结为以下四个条件:

  1. 互斥条件:每个资源每次只能被一个线程使用。
  2. 持有和等待:一个线程持有某些资源,并且等待其他线程释放自己需要的资源。
  3. 不剥夺:已获得的资源在未使用完之前不能被强制剥夺。
  4. 循环等待:两个或多个线程形成了循环等待的关系。

这四个条件是死锁发生的必要条件。如果这些条件都满足,那么死锁就可能发生了。

一个死锁的简单示例

我们来看一个非常简单的死锁示例。这段代码包含了两个线程,每个线程都试图获取两个锁,但是获取锁的顺序正好相反,最终造成了死锁。

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();
    }
}

代码分析

  1. 线程1先获取lockA,然后等待获取lockB
  2. 线程2先获取lockB,然后等待获取lockA

这样就产生了死锁。因为线程1和线程2都在等待对方释放锁,导致程序卡在这里,永远也无法向前推进。

如何定位死锁?

死锁发生时,线程会处于BLOCKED状态。要想定位这些问题,我们常用一些工具,比如jstack或者JConsole

使用 jstack 定位死锁

首先,你需要知道哪个进程发生了死锁。可以使用jps命令或操作系统的任务管理器找到Java进程的PID。然后,通过jstack命令来打印线程栈:

jstack <pid>

jstack会打印出当前所有线程的堆栈信息。如果发生了死锁,jstack会在输出中标明哪些线程处于死锁状态,类似下面这样的输出:

Found one Java-level deadlock:
=============================
"Thread1" (pid=1234, thread TID=0x00007f82a0133800, state=BLOCKED):
waiting to lock monitor 0x00007f828c0c0c00 (object 0x00007f828c0b2e30, a java.lang.String),
which is held by "Thread2"
"Thread2" (pid=1234, thread TID=0x00007f82a0135400, state=BLOCKED):
waiting to lock monitor 0x00007f828c0b0b00 (object 0x00007f828c0b2e30, a java.lang.String),
which is held by "Thread1"

JConsole图形化工具

JConsole是JDK自带的一个图形化监控工具,它可以通过JMX协议实时查看JVM状态。在死锁发生时,JConsole会自动显示死锁的线程,并且提供详细的信息,比如线程名称、占用的锁、等待的锁等。

如何修复死锁?

修复死锁的根本方法就是打破循环依赖关系。下面是几种常见的预防死锁的策略:

1. 避免锁的嵌套

尽量避免在同一线程中获取多个锁。如果必须获取多个锁,确保获取锁的顺序是一致的。比如,先获取lockA,再获取lockB,而不是反过来。

2. 使用tryLock()方法

tryLock()方法是java.util.concurrent.locks.Lock接口中的一部分,它允许线程尝试获取锁,但不会阻塞。如果获取锁失败,线程可以选择执行其他操作或者稍后再试,从而避免死锁。

Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();

boolean acquiredLockA = lockA.tryLock();
boolean acquiredLockB = lockB.tryLock();

if (acquiredLockA && acquiredLockB) {
    try {
        // 执行任务
    } finally {
        lockA.unlock();
        lockB.unlock();
    }
else {
    // 锁获取失败,执行其他操作
}

3. 设置锁的超时时间

通过设置锁的超时时间,可以避免一个线程长期等待锁。比如在使用ReentrantLock时,可以指定tryLock(long time, TimeUnit unit),这样如果线程在指定时间内未能获取到锁,就会放弃,避免死锁的发生。

boolean acquired = lockA.tryLock(500, TimeUnit.MILLISECONDS);

4. 使用更高层次的并发工具

Java提供了多种高层次的并发工具,例如ExecutorServiceCountDownLatchSemaphore等,它们通过内部的实现避免了直接管理锁的复杂性,从而减少了死锁的风险。

死锁是多线程编程中的一个常见问题,但我们只要理解死锁发生的条件,并掌握一些常见的工具和技术,就能够高效地定位和修复死锁问题。

最重要的是在编程中尽量避免死锁,通过合理的锁顺序、适时的锁释放以及使用高层次的并发工具,来让我们的程序更加健壮和高效。

-END-


ok,今天先说到这,老规矩,给大家分享一份不错的副业资料,感兴趣的同学找我领取。

以上,就是今天的分享了,看完文章记得右下角给何老师点赞,也欢迎在评论区写下你的留言

程序员老鬼
10年+老程序员,专注于AI知识普及,已打造多门AI课程,本号主要分享国内AI工具、AI绘画提示词、Chat教程、AI换脸、Chat中文指令、Sora教程等,帮助读者解决AI工具使用疑难问题。
 最新文章