死锁、如何定位、修复!

52 阅读4分钟

死锁

线程A持有锁A 线程B持有锁B  在线程A还没有释放锁A的情况下 线程B试图去获取锁A 在线程B还没有释放锁B的情况下 线程A去获取锁B 就形成了相互持有对方所需要的锁 而处于永久阻塞的状态 即为死锁

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


这个程序定义了一个DeadLockSample类,它继承了Thread类并覆盖了run方法。在run方法中,线程首先尝试获取first对象的锁,然后尝试获取second对象的锁。在main方法中,创建了两个DeadLockSample实例,它们分别以不同的顺序尝试获取两个锁。这种不同顺序的锁获取可能导致死锁,因为每个线程都在等待另一个线程释放它所持有的锁。

关于死锁的避免,根据您提供的内容,以下是一些建议:

避免滥用锁。同一把锁的加锁和解锁应该放在同一个方法中。尽量避免在持有一把锁的情况下获取另一把锁。

调用 jstack 获取线程栈:

image.png

如果我们是开发自己的管理工具,需要用更加程序化的方式扫描服务进程、定位死锁,可以考虑使用 Java 提供的标准管理 API,ThreadMXBean,其直接就提供了 findDeadlockedThreads​() 方法用于定位。为方便说明,我修改了 DeadLockSample,请看下面的代码片段。

public static void main(String[] args) throws InterruptedException {

  ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
  Runnable dlCheck = new Runnable() {

      @Override
      public void run() {
          long[] threadIds = mbean.findDeadlockedThreads();
          if (threadIds != null) {
                     ThreadInfo[] threadInfos = mbean.getThreadInfo(threadIds);
                     System.out.println("Detected deadlock threads:");
              for (ThreadInfo threadInfo : threadInfos) {
                  System.out.println(threadInfo.getThreadName());
              }
          }
       }
    };

       ScheduledExecutorService scheduler =Executors.newScheduledThreadPool(1);
       // 稍等5秒,然后每10秒进行一次死锁扫描
        scheduler.scheduleAtFixedRate(dlCheck, 5L, 10L, TimeUnit.SECONDS);
// 死锁样例代码…
}

image.png

重新编译执行,你就能看到死锁被定位到的输出。在实际应用中,就可以据此收集进一步的信息,然后进行预警等后续处理。但是要注意的是,对线程进行快照本身是一个相对重量级的操作,还是要慎重选择频度和时机。

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

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

a.互斥条件,类似 Java 中 Monitor 都是独占的,要么是我用,要么是你用。

b.互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其他线程抢占。

c.循环依赖关系,两个或者多个个体之间出现了锁的链条环。

所以,我们可以据此分析可能的避免死锁的思路和方法。

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

2.如果必须使用多个锁,尽量设计好锁的获取顺序。

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

类似 Object.wait(…) 或者 CountDownLatch.await(…),都支持所谓的 timed_wait,我们完全可以就不假定该锁一定会获得,指定超时时间,并为无法得到锁时准备退出逻辑。并发 Lock 实现,如 ReentrantLock 还支持非阻塞式的获取锁操作 tryLock(),这是一个插队行为(barging),并不在乎等待的公平性,如果执行时对象恰好没有被独占,则直接获取锁。有时,我们希望条件允许就尝试插队,不然就按照现有公平性规则等待,一般采用下面的方法: if (lock.tryLock() || lock.tryLock(timeout, unit)) { // ... }

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