面试官:请手写一段必然死锁的代码
前言
死锁(Deadlock),是并发编程中最需要考虑的问题之一,一般来说死锁发生的概率相对较小,但是危害奇大。本篇主要讲解死锁相关的内容,包括死锁形成的必要条件、危害、如何避免等等。
死锁的定义
死锁(英语:Deadlock),又译为死结,计算机科学名词。当两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,就称为死锁。在多任务操作系统中,操作系统为了协调不同行程,能否获取系统资源时,为了让系统运作,必须要解决这个问题。
具体到线程死锁,也就是多个(大于等于2个)线程相互持有对方需要的资源,在没有外界干扰的情况下,会永远处于等待状态。
必然死锁的例子
先来看一个必然发生死锁的例子,来直观的感受一下死锁:
/**
* 必然死锁的例子
* @author sicimike
*/
public class DeadLock {
final private static Object lock1 = new Object();
final private static Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
// 先获取lock1
System.out.println("thread-1 get lock1");
try {
// 休眠200毫秒,让thread-2获取lock2
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
// 在lock1同步代码中获取lock2
System.out.println("thread-1 get lock2");
}
}
System.out.println("thread-1 finished");
}, "thread-1");
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
// 先获取lock2
System.out.println("thread-1 get lock2");
synchronized (lock1) {
// 在lock2同步代码中获取lock1
System.out.println("thread-2 get lock1");
}
}
System.out.println("thread-2 finished");
}, "thread-2");
thread1.start();
thread2.start();
}
}
执行结果:
thread-1 get lock1
thread-2 get lock2
例子共有6行输出,但是只输出了2行。不仅如此,用IDE运行该代码时,IDE永远不会执行结束。
对于本次运行结果,thread-1首先获取CPU时间片,开始执行,获取锁lock1,输出thread-1 get lock1
,然后休眠200毫秒。在200毫秒内thread-2获取CPU时间片,开始执行,获取lock2,输出thread-2 get lock2
。
此时tread-1必须获取lock2才能继续执行,执行完成才能释放自己持有的lock1。而thread-2同理,想要继续执行,必须先获取thread-1持有的lock1,执行完成才能释放lock2。就这样,两个线程发生了死锁。导致后续的代码都不会执行,之后的语句并不会输出。
死锁的危害
- 首先肯定是程序得不到正确的结果,因为处于死锁状态的线程无法处理原先分配的任务。
- 死锁会降低资源利用率,处于死锁状态的线程所持有的资源是不会释放的,更不能被别的线程利用,所以会导致资源的利用率降低
- 可能导致新的死锁产生,死锁的线程持有的资源,可能是系统非常宝贵且有限的资源,其他线程获取不到,依然可能会被死锁,产生多米诺骨牌效应
死锁产生的必要条件
死锁的危害非常巨大,是并发编程必须要考虑的问题。不过好在死锁的产生条件比较严苛,需要同时满足四个必要条件:
- 互斥条件:一个资源同时最多能被一个线程持有
- 请求与保持条件:一个线程因请求其他资源而被阻塞时,不会释放已持有的资源
- 不剥夺条件:线程执行完成之前,其他的线程不能抢占该线程持有的资源
- 循环等待条件:多个线程请求的资源形成一个等待环
只要其中一个不满足就不可能发生死锁。
再回过头来看看上文的实例是不是满足这四个条件,thread-1和thread-2所需的资源是lock1和lock2,都是互斥锁,满足互斥条件;thread-1和thread-2被阻塞后不会释放持有的锁,满足请求与保持条件;thread-1和thread-2都不能直接抢占对方持有的锁,满足不剥夺条件;thread-1需要thread-2持有的lock2,而thread-2需要thread-1持有的lock1,满足循环等待条件。
因为只有2个线程,所以循环等待条件不是很明显,可以把实例改成三个线程
/**
* 三个线程-必然死锁的例子
* @author sicimike
*/
public class ThreeThreadDeadLock {
final private static Object lock1 = new Object();
final private static Object lock2 = new Object();
final private static Object lock3 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
// 先获取lock1
System.out.println("thread-1 get lock1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
// 在lock1中获取lock2
System.out.println("thread-1 get lock2");
}
}
System.out.println("thread-1 finished");
}, "thread-1");
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
// 先获取lock2
System.out.println("thread-2 get lock2");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock3) {
// 在lock2中获取lock3
System.out.println("thread-2 get lock3");
}
}
System.out.println("thread-2 finished");
}, "thread-2");
Thread thread3 = new Thread(() -> {
synchronized (lock3) {
// 先获取lock3
System.out.println("thread-3 get lock3");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
// 在lock3中获取lock1
System.out.println("thread-3 get lock1");
}
}
System.out.println("thread-3 finished");
}, "thread-3");
thread1.start();
thread2.start();
thread3.start();
}
}
执行结果
thread-1 get lock1
thread-2 get lock2
thread-3 get lock3
同样,程序也不会结束。
thread-1获取lock1后还需要获取lock2,thread-2获取lock2后还需要lock3,thread-3获取lock3后还需要获取lock1,这样就是循环等待条件,三个线程所需要的资源形成了一个环。
定位死锁
主要讲解三种方式来定位死锁:jstack命令、jconsole工具、ThreadMXBean类,以上面两个线程死锁的实例演示。
-
jstack命令
先查看系统进程,jps(Java Virtual Machine Process Status Tool)是JDK提供的一个显示当前所有java进程pid的命令,位于...\jdk1.8.0_101\bin
目录C:\Users\Atao>jps 2272 10180 Jps 13956 RemoteMavenServer36 11032 Launcher 8488 DeadLock
可以很清楚的看到运行DeadLock.java类的进程pid(8488),再运行jstack命令,jstack是JDK提供的线程堆栈分析工具,使用该命令可以查看Java程序线程堆栈信息,位于
...\jdk1.8.0_101\bin
目录C:\Users\Atao>jstack -F 8488 Attaching to process ID 8488, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.101-b13 Deadlock Detection: Found one Java-level deadlock: ============================= "thread-1": waiting to lock aaa@qq.com (aaa@qq.com, a java/lang/Object), which is held by "thread-2" "thread-2": waiting to lock aaa@qq.com (aaa@qq.com, a java/lang/Object), which is held by "thread-1" Found a total of 1 deadlock. Thread 1: (state = BLOCKED) ......
很清楚的就能看到哪几个线程发生了死锁(现在应该知道为什么要给每个线程取一个有意义的名字了)
-
jconsole工具,位于
...\jdk1.8.0_101\bin
目录
启动jconsole
检测死锁
查看检测结果 -
ThreadMXBean类
ThreadMXBean
是JDK自带的类,位于java.lang.management
包中。是Java虚拟机线程的系统管理接口。以上文举出的必然死锁的例子为例:public class DeadLock { final private static Object lock1 = new Object(); final private static Object lock2 = new Object(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { synchronized (lock1) { // 先获取lock1 System.out.println("thread-1 get lock1"); try { // 休眠200毫秒,让thread-2获取lock2 Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { // 在lock1同步代码中获取lock2 System.out.println("thread-1 get lock2"); } } System.out.println("thread-1 finished"); }, "thread-1"); Thread thread2 = new Thread(() -> { synchronized (lock2) { // 先获取lock2 System.out.println("thread-2 get lock2"); synchronized (lock1) { // 在lock2同步代码中获取lock1 System.out.println("thread-2 get lock1"); } } System.out.println("thread-2 finished"); }, "thread-2"); // 检测死锁的线程 Thread monitorThread = new Thread(() -> { while (true) { try { // 每隔2秒检测一次 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("死锁线程信息:"); ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); long[] deadlockedThreads = threadMXBean.findDeadlockedThreads(); for (long thread : deadlockedThreads) { System.out.println(threadMXBean.getThreadInfo(thread)); } } }, "monitor-thread"); thread1.start(); thread2.start(); monitorThread.start(); } }
执行结果:
thread-1 get lock1 thread-2 get lock2 死锁线程信息: "thread-2" Id=12 BLOCKED on aaa@qq.com owned by "thread-1" Id=11 "thread-1" Id=11 BLOCKED on aaa@qq.com owned by "thread-2" Id=12 死锁线程信息: "thread-2" Id=12 BLOCKED on aaa@qq.com owned by "thread-1" Id=11 "thread-1" Id=11 BLOCKED on aaa@qq.com owned by "thread-2" Id=12 ......
死锁的处理
既然知道了死锁产生的四个必要条件,所以只需要破坏其中一个或者多个即可。
- 对于互斥条件,想要破坏它,就是能加共享锁的就不要加独占锁
- 对于请求与保持条件,可以设置超时时间,阻塞一段时间后,如果还未获取到锁,就释放自己持有的锁;或者死锁发生时,调度者强行中断某个死锁的状态,并释放持有的资源
- 对于不剥夺条件,请求资源(锁)时,使用可以响应中断的锁,例如
Lock.lockInterruptibly()
- 对于循环等待条件,这个条件相对来说是最好破坏的。只需要打破等待环即可。线程请求多把锁的时候,做到按顺序请求锁(每个线程),这样就不会形成等待环。
可以动手改造下上文中三个线程死锁的例子,使三个线程均按照lock1->lock2->lock3的顺序请求锁。
最佳实践
死锁的处理策略总的来说有三种方式:
- 避免策略
- 检测与修复
- 不处理(鸵鸟策略)
对于生产环境而言,死锁的防大于治,也就是说应该将重点放在死锁的避免上。在实际工作中养成良好的习惯,可以大大减少死锁发生的概率,好的习惯总结如下:
- 争抢锁时设置超时,比如Lock的tryLock(long, unit)方法
- 多使用并发工具类,而不是自己设计锁
- 尽量降低锁的粒度
- 如果能使用同步代码块,就不使用同步方法:锁的粒度更小;还可以自己指定锁对象
- 给线程起有意义的名字:方便debug、日志记录
- 避免锁的嵌套
- 尽量不要多个功能用同一把锁:专锁专用
- 分配资源前先看能不能收回来:银行家算法
总结
死锁像火灾一样:不可预测、蔓延迅速、危害大。编写并发程序的时候,一定要特别关注。
扩展阅读
- https://zh.wikipedia.org/zh-hans/银行家算法
- https://zh.wikipedia.org/wiki/哲学家就餐问题
上一篇: Java内存分配 博客分类: Java基础 java内存堆栈JVM
下一篇: 手写java死锁及排查
推荐阅读