线程总结(五):死锁

我们理解了“同步锁”的概念:线程先获取锁,然后再操作共享数据,线程执行完成,释放锁,其他线程获取锁,去操作共享数据。

这个时候就容易出现一个问题,线程 A 在等待线程 B 执行完,而线程 B 却在等待线程 C 执行完,恰好线程 C 又在等待线程 A 执行完,就像一条蛇咬住自己的尾巴一样,这个时候 A、B、C 三个线程都进入阻塞状态,他们都在等待某个同步锁被释放,此时三个线程被无限阻塞,我们称之为“死锁”。

产生死锁的条件

Java 产生死锁有四个条件

  1. 互斥使用,即当资源被一个线程占用时,其他线程不能使用。
  2. 不可抢占,一个线程不能从另一个线程手中多去共享资源的使用权,只能等着对方释放。
  3. 请求和保持,即当一个线程在请求其他资源的同时还保持着对另一个资源的占有。
  4. 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

当上述四个条件全部成立的时候,死锁就形成了。

举一个不太恰当的例子:

public class DeadLock {
    public static void main(String[] args) {
        Bike bike = new Bike();
        Motor motor = new Motor();

        Thread thread1 = new FixBikeThread(bike, motor);
        Thread thread2 = new FixBikeThread(bike, motor);
        thread1.setName("王大锤");
        thread2.setName("孔连顺");

        thread1.start();
        thread2.start();
    }
}

class FixBikeThread extends Thread {

    private Bike bike;
    private Motor motor;

    public FixBikeThread(Bike bike, Motor motor) {
        this.bike = bike;
        this.motor = motor;
    }

    @Override
    public void run() {
        bike.fixTire(motor);
        motor.fixTire(bike);
    }
}

class Bike {
    public synchronized void fixTire(Motor motor) {
        System.out.println(Thread.currentThread().getName() + " 修理【自行车】的【轮胎】");
        System.out
                .println(Thread.currentThread().getName() + " 要去修理【摩托】的【铃铛】了");

        motor.fixBell();
    }

    public synchronized void fixBell() {
        System.out.println(Thread.currentThread().getName() + " 修理【自行车】的【铃铛】");
    }
}

class Motor {
    public synchronized void fixTire(Bike bike) {
        System.out.println(Thread.currentThread().getName() + " 修理【摩托】的【轮胎】");
        System.out.println(Thread.currentThread().getName()
                + " 要去修理【自行车】的【铃铛】了");
        bike.fixBell();
    }

    public synchronized void fixBell() {
        System.out.println(Thread.currentThread().getName() + " 修理【摩托】的【铃铛】");
    }
}

执行结果是:

王大锤 修理【自行车】的【轮胎】
王大锤 要去修理【摩托】的【铃铛】了
王大锤 修理【摩托】的【铃铛】
孔连顺 修理【自行车】的【轮胎】
王大锤 修理【摩托】的【轮胎】
孔连顺 要去修理【摩托】的【铃铛】了
王大锤 要去修理【自行车】的【铃铛】了

这下就进入死锁了...

来分析一下原因:

王大锤 线程开始执行,调用 Bike 对象的修理轮胎方法。

摩托车和自行车的修理铃铛和修理轮胎都是 synchronized 修饰的方法,所以他们的同步锁就是调用他们的对象。

王大锤线程调用了 Bike 对象的修理轮胎方法,那么则获取了 Bike 对象的占有权限,其他线程(孔连顺)必须等王大锤执行完之后退出释放锁之后才能占用。

王大锤执行自行车的修理轮胎方法,输出:==王大锤 修理【自行车】的【轮胎】.==

紧接着输出:==王大锤 要去修理【摩托】的【铃铛】了==

然后调用摩托车的修理铃铛方法,这个方法也是 synchronized 方法,所以王大锤获取了 Motor 对象的占有权,同时释放了 Bike 对象的占有权(因为执行完该对象的方法了啊),王大锤执行摩托的修理铃铛方法。

输出:==王大锤 修理【摩托】的【铃铛】==

这个时候孔连顺线程开始执行(确切的说,应该是这个时候因为王大锤释放了自行车的占用权,所以孔连顺获取了自行车权限),孔连顺开始执行自行车的修理轮胎方法:

输出:==孔连顺 修理【自行车】的【轮胎】==

王大锤又执行了摩托的修理论坛方法:

输出:==王大锤 修理【摩托】的【轮胎】==

孔连顺修理完自行车的轮胎,也要去修理摩托车的铃铛了

输出:==孔连顺 要去修理【摩托】的【铃铛】了==

但是这个时候摩托车的占用权还在王大锤手里呢,所以他等着王大锤释放摩托的占用权。

王大锤在修理完摩托的轮胎之后,要去修理自行车的铃铛了

输出:==王大锤 要去修理【自行车】的【铃铛】了==

但是这个时候自行车的还在孔连顺手里呢,所以他等着孔连顺释放自行车的占用权。

他俩都等着对方释放释放手里的车的占有权,这特么就尴尬了...僵着吧!

文章一开始讲了死锁的四个条件,分别看一下满不满足:

  • 互斥使用,即当资源被一个线程占用时,其他线程不能使用。
    满足,因为 Bike 和 Motor 中的方法都是 synchronized 修饰的,所以当调用该方法的时候,其同步锁就是调用他们的对象,哪个线程调用该对象的方法,哪个线程占用占用同步锁,其他线程无法访问。
  • 不可抢占,一个线程不能从另一个线程手中多去共享资源的使用权,只能等着对方释放。
    满足,理由同上
  • 请求和保持,即当一个线程在请求其他资源的同时还保持着对另一个资源的占有。
    满足,王大锤手持摩托去请求自行车,孔连顺手持自行车请求摩托。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
    满足,理由同上

死锁的检测

方法一:JConsile

JDK 给我们提供了检测死锁的工具:jconsole

我们运行程序之后通过 CMD 打开该工具,就会出现项目的进程:

点击连接,连接到进程,然后点开线程选项卡

点击下面的检测死锁,jconsole就会给我们检测出该线程中造成死锁的线程,点击选中即可查看详情:

然后我们就可以看到造成死锁的原因了。

方法二:直接使用 JVM 自带的命令

  • 首先,通过 jps 命令查看我们的 java 进程
12488 Jps
12376 DeadLock
3700
  • 可以看到我们的需要查看的线程 vmid 号为 12488
  • 通过 jstack -l xxxxx 命令查看该进程的堆栈情况
C:\Users\LGB>jstack -l 12488
12488: no such process

C:\Users\LGB>jstack -l 12376
2017-01-05 00:46:16
Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode):

"DestroyJavaVM" prio=6 tid=0x000000000258e000 nid=0x3074 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
        - None

"孔连顺" prio=6 tid=0x000000000c5c3800 nid=0x30b0 waiting for monitor entry [0x000000000cc2e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.lixyz.threadtest.Motor.fixBell(DeadLock.java:58)
        - waiting to lock <0x00000007d5d9da80> (a com.lixyz.threadtest.Motor)
        at com.lixyz.threadtest.Bike.fixTire(DeadLock.java:41)
        - locked <0x00000007d5d9bf40> (a com.lixyz.threadtest.Bike)
        at com.lixyz.threadtest.FixBikeThread.run(DeadLock.java:30)

   Locked ownable synchronizers:
        - None

"王大锤" prio=6 tid=0x000000000c5c0800 nid=0x30ac waiting for monitor entry [0x000000000ce9f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.lixyz.threadtest.Bike.fixBell(DeadLock.java:45)
        - waiting to lock <0x00000007d5d9bf40> (a com.lixyz.threadtest.Bike)
        at com.lixyz.threadtest.Motor.fixTire(DeadLock.java:54)
        - locked <0x00000007d5d9da80> (a com.lixyz.threadtest.Motor)
        at com.lixyz.threadtest.FixBikeThread.run(DeadLock.java:31)

是不是也一目了然?

使用 ThreadMXBean 接口编码检测死锁

ThreadMXBean 是 Java 虚拟机线程系统的管理接口,此接口的实现是一个 MXBean,可以通过调用 ManagementFactory.getThreadMXBean() 来获取它。

它为我们提供了很多游泳的操作方便检测程序性能,在这里我们终点关注:

  • findMonitorDeadlockedThreads
  • findDeadlockedThreads

二者的区别在于 findMonitorDeadlockedThreads 只可以查找涉及对象监视器的死锁。要想查看要查找涉及对象监视器和可拥有同步器的死锁,应该使用 findDeadlockedThreads 方法。

他们的返回值是处于死锁状态的线程 ID 数组,如果不存在死锁,则返回 null。

因为只能检测却并不能解决,并且开销很大,所以一般情况下我们把对死锁的检测封装成一个可重用的组件里并且周期性的去启动他。

ThreadMXBean 简单使用方法:

        while (true) {
            try {
                ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
                long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
                System.out.println("==========出现死锁的线程有==========");
                if (deadlockedThreads != null) {
                    for (int i = 0; i < deadlockedThreads.length; i++) {
                        System.out.println(ThreadUtilities.getThread(
                                deadlockedThreads[i]).getName());
                    }
                }
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

输出内容:

==========出现死锁的线程有==========
孔连顺
王大锤

如何避免死锁

事实上,将最开始的代码稍微修改一下就可以避免死锁的产生:

public class DeadLock {
    public static void main(String[] args) {
        Bike bike = new Bike();
        Motor motor = new Motor();

        Thread thread1 = new FixBikeThread(bike, motor);
        Thread thread2 = new FixBikeThread(bike, motor);
        thread1.setName("王大锤");
        thread2.setName("孔连顺");

        thread1.start();
    
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
    
        thread2.start();
    }
}

在第一个线程休眠 5 秒之后再启动第二个线程,就不会出现死锁了,其原因在于在第二个线程之前的时候第一个线程已经执行完了,所以不会造成对同步锁的争用。

还记得之前讲线程控制的时候的 join 方法么?它的作用是等待一个线程执行,恰好可以拥在这里嘛。

public class DeadLock {
    public static void main(String[] args) {
        Bike bike = new Bike();
        Motor motor = new Motor();

        Thread thread1 = new FixBikeThread(bike, motor);
        Thread thread2 = new FixBikeThread(bike, motor);
        thread1.setName("王大锤");
        thread2.setName("孔连顺");

        try {
            thread1.start();
            thread1.join();
            thread2.start();
        } catch (InterruptedException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }
    }
}

我们在主线程中调用 thread1 的 join 方法,所以主线程需要等待 thread1 执行完之后才会调用线程 thread2 的 start() 方法,这样不就避免了同步锁的争用了嘛!!

事实上,造成死锁的四个条件只要干掉一个就可避免死锁的产生了!