一篇文章搞清JVM死锁问题及排查

关于死锁,一直是面试和日常开发中的熟悉话题,本文将进行一下探讨:

  1. 什么是死锁
  2. 出现死锁的原因
  3. 如何避免死锁
  4. 代码中死锁问题怎么排查

@

目录

1. 什么是死锁

死锁是指两个或两个以上的进程或线程,在执行过程中,由于竞争资源而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去

划重点:两个或两个以上进程或线程,竞争资源,最终阻塞无法进行下去

这里可能会问:外力作用是什么外力?资源剥夺;撤销进程;进程回退等

2. 出现死锁的原因

  1. 系统资源不足;

  2. 进程推进顺序非法

  3. 系统资源分配不当。

    另外,信号量使用不当也会造成死锁。比如A等B消息,B等A的消息

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

3. 如何预防和避免死锁

  • 预防:

死锁的预防基本思想打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。

比如:

打破互斥条件:允许进程同时访问某些资源

打破不剥夺条件:允许进程从占有者占有的资源中强行剥夺一些资源

打破请求与保持条件:进程在运行前一次性地向系统申请它所需要的全部资源

打破循环等待条件:实行资源有序分配策略

  • 避免:
  1. 加锁顺序(线程按照一定的顺序加锁)
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  3. 死锁检测

阿里巴巴中最新的开发规约,里面有对避免死锁的说明,具体如下:


【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。 说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。

4. 实战JVM死锁问题排查

4.1 死锁代码案例

按照死锁产生原则,可写出一个产生死锁的程序

public class DeadLock {
    //创建两个对象,用两个线程分别先后独占
    private Boolean flag1 = true;
    private Boolean flag2 = false;

    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程1开始,作用是当flag1 = true 时,将flag2也改为 true");
                synchronized (deadLock.flag1){
                    if(deadLock.flag1){
                        try{
                            //睡眠1s ,模拟业务执行耗时,并保证两个线程进入死锁状态
                            Thread.sleep(1000);
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                        System.out.println("flag1 = true,准备锁住flag2...");
                        synchronized (deadLock.flag2){
                            deadLock.flag2 = true;
                        }
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程2开始,作用是当flag2 = false 时,将flag1也改为 false");
                synchronized (deadLock.flag2){
                    if(!deadLock.flag2){
                        try{
                            //睡眠1s ,模拟业务执行耗时,并保证两个线程进入死锁状态
                            Thread.sleep(1000);
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                        System.out.println("flag2 = false,准备锁住flag1...");
                        synchronized (deadLock.flag1){
                            deadLock.flag1 = false;
                        }

                    }
                }
            }
        }).start();
    }
}

以上代码,可以用一个死锁的图解释。线程1独占对象1,想要访问对象2,而对象2此时已经独占对象2,在等待对象1的资源释放,此时线程1因无法获取到对象2而无法向下执行,因此没法释放对象1,线程2同理,造成了死锁状态,两个线程都阻塞在等待资源处

4.2 死锁问题JVM工具排查

4.2.1 jps+jstack方式排查

  1. 查找程序运行端口

    > jps -l 
    18714 sun.tools.jps.Jps
    18703 jvm.DeadLock
  2. jstack打印堆栈信息,发现死锁存在的位置,进行排查

    > jstack -l 18703
一篇文章搞清JVM死锁问题及排查

4.2.2 jconsole方式排查

mac下:输入jconsol命令通过可视化界面连接

一篇文章搞清JVM死锁问题及排查

选择线程,监测死锁。会将死锁的线程信息都展示出来
一篇文章搞清JVM死锁问题及排查
一篇文章搞清JVM死锁问题及排查

4.2.3 jvisualvm方式

用命令行召唤出jconsole,选择对应进程即可直观看到死锁的存在

一篇文章搞清JVM死锁问题及排查