前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >什么情况下Java程序会产生死锁?如何定位、修复?

什么情况下Java程序会产生死锁?如何定位、修复?

作者头像
王小明_HIT
发布2020-05-08 18:11:43
1.4K0
发布2020-05-08 18:11:43
举报
文章被收录于专栏:程序员奇点程序员奇点

什么情况下Java程序会产生死锁?如何定位、修复?

死锁

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

产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

image

定位死锁

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

针对死锁,可以深入考察

  • 抛开字面上的概念,让面试者写一个可能死锁的程序,顺便也考察下基本的线程编程。
  • 诊断死锁有哪些工具,如果是分布式环境,可能更关心能否用API实现吗?
  • 后期诊断死锁还是挺痛苦的,经常加,如何在编程中尽量避免一些典型场景的死锁,有其他工貝辅助吗?

写个死锁的程序

代码语言:javascript
复制
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;
    }
    
    /**
     * @see java.lang.Thread#run()
     */
    @Override
    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) {
            e.printStackTrace();
        }
       }
    }

    /**
     * @param args
     * @throws InterruptedException
     */
    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();
    }

}

运行结果

代码语言:javascript
复制
Thread2 obtained lockB
Thread1 obtained lockA

jstack 线程 dump 文件分析

image

最后,结合代码分析线程栈信息。上面这个输出非常明显,找到处于BLOCKED状态的线程,按照试图获取( waiting)的锁ID(请看我标记为相同颜色的数字)查找,很快就定位问题。jstack本身也会把类似的简单死锁抽取出来,直接打印出来在实际应用中,类死锁情况未必有如此清晰的输出,但是总体上可以理解为区分线程状态->查看等待目标->对比 Monitor等持有状态。

写个定位死锁的方法

使用 java 握倛的标准管理API, ThreadMXBean,其直接就提供了 findDeadlockedthreads() 方法

  • https://docs.oracle.com/javase/9/docs/api/java/lang/management/ThreadMXBean.html#findDeadlockedThreads--
代码语言:javascript
复制
public class DeadLockFinderSample extends Thread {
    private String first;

    private String second;

    public DeadLockFinderSample(String name, String first, String second) {
        super(name);
        this.first = first;
        this.second = second;
    }

    /**
     * @see java.lang.Thread#run()
     */
    @Override
    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) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        
        ThreadMXBean mbean = (ThreadMXBean) 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);
        // 等待1s 每2s 进行一次死锁扫描
        scheduler.scheduleAtFixedRate(dlcheck, 1L, 2L, TimeUnit.SECONDS);
        
        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();
    }

}

运行结果

代码语言:javascript
复制
Thread1 obtained lockA
Thread2 obtained lockB
Detected deadlock threads:
Thread2
Thread1
Detected deadlock threads:
Thread2
Thread1
Detected deadlock threads:
Thread2
Thread1

如何预防死锁

死锁的4个条件

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

方法一

避免使用多个锁, 并且只有需要时才持有锁,嵌套的 synchronized 或者 lock 非常容易出现问题。

方法二

如果必须要使用锁,尽量设计好锁的获取顺序,可以参考银行家算法。

  • https://en.wikipedia.org/wiki/Banker%27s_algorithm
  • 一般的情况,我建议可以采取些简单的辅助手段,比如:将对象(方法)和锁之间的关系,用图形化的方式表示分别抽取出来,以今天最初的死锁为例,因为是调用了同一个线程所以更加简单。

image

  • 然后根据对象之间组合、调用的关系对比和组合,考虑可能调用时序。

image

  • 按照可能时序合并,发现可能死锁的场景。

image

方法三

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

代码语言:javascript
复制
if(lock.tryLock() || lock.tryLock(timeout, unit)){
    // ...
}

方法四

业界也有一些其他方面的尝试,比如通过静态代码分析(如 FindBugs)去查找固定的模式,进而定位可能的死锁或者竟争情况。实践证明这种方法也有一定作用,请參考担关文档。除了典型应用中的死锁场景,其实还有一些更令人头疼的死锁,比如类加载过程发生的死锁,尤其是在框架大量使用自定义类加载时,因为往往不是在应用本身的代码库中, jstack等工具也不见得能够显示全部锁信息,所以处理起来比较棘手。对此,Java有方文档进行了详细解释。

  • https://docs.oracle.com/javase/7/docs/technotes/guides/lang/cl-mt.html
  • https://plugins.jetbrains.com/plugin/3847-findbugs-idea
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员奇点 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么情况下Java程序会产生死锁?如何定位、修复?
    • 死锁
      • 产生死锁的四个必要条件:
      • 定位死锁
    • 写个定位死锁的方法
      • 如何预防死锁
        • 方法一
        • 方法二
        • 方法三
        • 方法四
    相关产品与服务
    腾讯云代码分析
    腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档