java多线程-死锁分析

死锁概念

所谓的死锁指的是多个线程之间因为竞争同一系统资源从而造成的一种僵局(互相等待)现象。此时若无外力作用,这些线程都将无法继续往下执行。

比如:线程A和线程B互相等待对方持有的锁从而导致程序无限死循环下去

死锁重现

实现死锁的步骤分三步走:

1、两个线程里面分别持有两个对象锁:lock1和lock2,这两个lock对象作为同步代码块的锁
2、在线程1的run()方法中使用同步块先获取lock1的对象锁,之后线程休眠,休眠时间不需要太长,50毫秒足够;然后再接着获取lock2的对象锁,这样做的目的主要是为了防止线程1启动之后一下子就连续获得了lock1和lock2两个对象的锁使得无法重现死锁
3、在线程2的run()方法中使用同步块先获取lock2的对象锁,之后线程休眠,然后再接着获取lock1的对象锁,当然,此时lock1的对象锁已经被线程1持有,线程2必然是要等到线程1释放lock1之后才能获取

代码实现

我们编写一个MyDeadLockTest.java

import java.util.concurrent.TimeUnit;

/**
 * 测试死锁:
 * 1、首先,leftLock线程代理类启动的时候,先获取left对象锁,之后休眠2秒
 * 2、接着,rightLock线程代理类启动的时候,先获取right对象锁,之后休眠2秒
 * 3、leftLock休眠结束后,需要先获取right对象锁才能继续执行,而此时right已被rightLock锁定
 * 4、rightLock休眠结束后,需要先获取left对象锁才能继续执行,而此时left已被leftLock锁定
 * 5、于是,leftLock和rightLock互相等待,都需要等待对方先释放锁从而获得锁资源继而才能继续往下执行,
 * 此时如果没有外力的作用是不可能做到的,因此导致了死锁的情况
 * Created by feizi on 2018/5/25.
 */
public class MyDeadLockTest {
    public static void main(String[] args) {
        MyDeadLock lock = new MyDeadLock();
        ProxyLeftLock leftLock = new ProxyLeftLock(lock);
        ProxyRightLock rightLock = new ProxyRightLock(lock);

        leftLock.start();
        rightLock.start();
    }
}

/**
 * 死锁例子
 */
class MyDeadLock {
    private final Object left = new Object();
    private final Object right = new Object();

    /**
     * 左边
     * @throws InterruptedException
     */
    public void left() throws InterruptedException {
        synchronized (left){
            TimeUnit.SECONDS.sleep(2);
            synchronized (right){
                System.out.println("左边...");
            }
        }
    }

    /**
     * 右边
     */
    public void right() throws InterruptedException {
        synchronized (right){
            TimeUnit.SECONDS.sleep(2);

            synchronized (left){
                System.out.println("右边...");
            }
        }
    }
}

/**
 * 多线程执行代理类-左边
 */
class ProxyLeftLock extends Thread{
    private MyDeadLock lock;

    public ProxyLeftLock(MyDeadLock lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            lock.left();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 多线程执行代理类-右边
 */
class ProxyRightLock extends Thread {
    private MyDeadLock lock;

    public ProxyRightLock(MyDeadLock lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            lock.right();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

之后,把我们编写的这个类丢到Linux环境下编译运行一下试试。

  1. 首先执行javac MyDeadLockTest.javajava文件编译成class文件

    1.jpg

  2. 然后执行java MyDeadLockTest命令并查看运行结果

    2.jpg

  3. 结果我们看到执行上述命令之后什么也没打印,因为此时已经形成了死锁,程序卡住了。接下来我们来验证一下,此时不要执行ctrl + c退出,我们另开一个窗口执行jps命令获取当前Java虚拟机进程的pid.

    3.jpg

  4. 执行jstack pid命令打印线程堆栈信息,不出意外的话我们将看到如下内容:

    4.jpg

  5. 在上述堆栈信息的最后,我们看到分析结果:发现了一个java级别的死锁,并且显示出了具体引起死锁的代码行数在MyDeadLockTest的第39行和第52行。

    5.jpg

6、回过头来我们看下具体的代码,找到MyDeadLockTest的第39行和第52行。

10.jpg

从上面打印的堆栈信息,其实我们就可以看出程序已经发生死锁了。

"Thread-1" prio=10 tid=0x00007fc4880a7800 nid=0x7cd7 waiting for monitor entry [0x00007fc47e762000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at MyDeadLock.right(MyDeadLockTest.java:52)
    - waiting to lock <0x00000007d704ca30> (a java.lang.Object)
    - locked <0x00000007d704ca40> (a java.lang.Object)
    at ProxyRightLock.run(MyDeadLockTest.java:91)

"Thread-0" prio=10 tid=0x00007fc4880a5800 nid=0x7cd6 waiting for monitor entry [0x00007fc47e863000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at MyDeadLock.left(MyDeadLockTest.java:39)
    - waiting to lock <0x00000007d704ca40> (a java.lang.Object)
    - locked <0x00000007d704ca30> (a java.lang.Object)
    at ProxyLeftLock.run(MyDeadLockTest.java:71)

死锁原因分析

首先,我们仅从代码的层面推测一下产生死锁的原因:

1、首先,leftLock线程代理类启动的时候,先获取left对象锁,之后休眠2秒
2、接着,rightLock线程代理类启动的时候,先获取right对象锁,之后休眠2秒
3、leftLock休眠结束后,需要先获取right对象锁才能继续执行,而此时right已被rightLock锁定
4、rightLock休眠结束后,需要先获取left对象锁才能继续执行,而此时left已被leftLock锁定
5、于是,leftLock和rightLock互相等待,都需要等待对方先释放锁从而获得锁资源继而才能继续往下执行,此时如果没有外力的作用是不可能做到的,因此导致了死锁的情况

接下来我们来分别介绍一下每一部分的意思,以上面的"Thread-1"为例:

1、"Thread-1"表示线程名称,
2、prio=10表示线程优先级,这里为10
3、tid=0x00007fc4880a7800表示线程ID
4、nid=0x7cd7表示线程对应的本地线程ID,Linux环境下可以使用top -Hp JVM进程Id来查看JVM进程下的本地线程(也被称作LWP)信息,注意这个本地线程是用十进制表示的,nid是用16进制表示的,转换一下就行了。
5、0x00007fc47e762000表示给线程分配的内存地址
6、java.lang.Thread.State: BLOCKED表示当前线程所处的状态

从上面打印的线程堆栈信息我们可以看出,Thread-1处于BLOCKED阻塞状态中,Thread-0也处于BLOCKED阻塞状态中,并且:

1、Thread-1锁住了<0x00000007d704ca40>,等待获取锁<0x00000007d704ca30>
2、Thread-0锁住了<0x00000007d704ca30>,等待获取锁<0x00000007d704ca40>

由于Thread-1Thread-0这两个线程同时都在等待获取对方持有的锁,所以就这么永久地等待下去了,从而造成了死锁。

最后,我们安利一款JAVA性能分析工具(Java Mission Control),简称:JMC。教程请参考https://www.cnblogs.com/duanxz/p/8533174.htmlhttps://www.cnblogs.com/aurain/p/6178671.html这两篇文章。这个JMC是jdk自带的一个性能分析工具,功能非常强大,主要是对JVM进程进行监控,包括占用的CPU资源、内存分配情况,GC回收情况,线程运行情况都可以进行分析。

我们在idea中启动时,先配置JVM参数开启JFR

-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder

我们在idea中配置启动JVM参数信息


6.jpg

运行jdk安装目录下面的bin目录里面的jmc.exe程序


9.jpg

选择本地监控的java进程,可以查看具体的线程信息


7.jpg
8.jpg

从上面查看界面分析的结果,我们一样也看到了发生了死锁。

如何避免死锁

在有些情况下死锁是可以避免的,常用的避免死锁的技术有以下三种:

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

原文参考:

  1. https://www.cnblogs.com/xujingyang/p/6677160.html
  2. https://blog.csdn.net/ls5718/article/details/51896159

推薦閱讀更多精彩內容