标题: 面试官:请手写一段必然死锁的代码
作者: Sicimike
原文地址: https://blog.csdn.net/Baisitao_/article/details/103358978
前言
死锁(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-2 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。就这样,两个线程发生了死锁。导致后续的代码都不会执行,之后的语句并不会输出。
死锁的危害
- 首先肯定是 程序得不到正确的结果 ,因为处于死锁状态的线程无法处理原先分配的任务。
- 死锁会降低资源利用率 ,处于死锁状态的线程所持有的资源是不会释放的,更不能被别的线程利用,所以会导致资源的利用率降低
- 可能导致新的死锁产生 ,死锁的线程持有的资源,可能是系统非常宝贵且有限的资源,其他线程获取不到,依然可能会被死锁,产生多米诺骨牌效应
死锁产生的必要条件
死锁的危害非常巨大,是并发编程必须要考虑的问题。不过好在死锁的产生条件比较严苛,需要同时满足四个 必要 条件:
- 互斥条件 :一个资源同时最多能被一个线程持有
- 请求与保持条件 :一个线程因请求其他资源而被阻塞时,不会释放已持有的资源
- 不剥夺条件 :线程执行完成之前,其他的线程不能抢占该线程持有的资源
- 循环等待条件 :多个线程请求的资源形成一个等待环
张三思评
互斥:比如上面的例子中, lock1 可以被多个线程同时上锁,那么就不会出现这种情况了
请求与保持:这个可以理解, 请求就是要更多, 保持就是自己不扔掉,仅仅强调这个行为, 没有别的意思, 不是猴子掰玉米,掰一个,扔一个
不剥夺: 如果没人管,没有操作系统监控,用户不停止线程, 死锁就会一直保持…
循环等待: 互相惦记.
只要其中一个不满足就不可能发生死锁。
再回过头来看看上文的实例是不是满足这四个条件,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 Monitor@0x00000000173a0628 (Object@0x00000000d64dc960, a java/lang/Object),
which is held by "thread-2"
"thread-2":
waiting to lock Monitor@0x000000001739f188 (Object@0x00000000d64dc950, 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 java.lang.Object@793190d2 owned by "thread-1" Id=11
"thread-1" Id=11 BLOCKED on java.lang.Object@77f67184 owned by "thread-2" Id=12
死锁线程信息:
"thread-2" Id=12 BLOCKED on java.lang.Object@793190d2 owned by "thread-1" Id=11
"thread-1" Id=11 BLOCKED on java.lang.Object@77f67184 owned by "thread-2" Id=12
......
死锁的处理
既然知道了死锁产生的四个必要条件,所以只需要破坏其中一个或者多个即可。
- 对于互斥条件,想要破坏它,就是能加共享锁的就不要加独占锁
- 对于请求与保持条件,可以设置超时时间,阻塞一段时间后,如果还未获取到锁,就释放自己持有的锁;或者死锁发生时,调度者强行中断某个死锁的状态,并释放持有的资源
- 对于不剥夺条件,请求资源(锁)时,使用 可以响应中断的锁 ,例如
Lock.lockInterruptibly()
- 对于循环等待条件,这个条件相对来说是最好破坏的。只需要打破等待环即可。线程请求多把锁的时候,做到按顺序请求锁(每个线程),这样就不会形成等待环。
可以动手改造下上文中三个线程死锁的例子,使三个线程均按照lock1->lock2->lock3的顺序请求锁。
最佳实践
死锁的处理策略总的来说有三种方式:
- 避免策略
- 检测与修复
- 不处理(鸵鸟策略)
对于生产环境而言,死锁的 防 大于 治
,也就是说应该将重点放在死锁的避免上。在实际工作中养成良好的习惯,可以大大减少死锁发生的概率,好的习惯总结如下:
- 争抢锁时 设置超时 ,比如Lock的tryLock(long, unit)方法
- 多使用 并发工具类 ,而不是自己设计锁
- 尽量 降低锁的粒度
- 如果能使用 同步代码块 ,就不使用同步方法:锁的粒度更小;还可以自己指定锁对象
- 给线程起有意义的名字 :方便debug、日志记录
- 避免锁的嵌套
- 尽量不要多个功能用同一把锁:专锁专用
- 分配资源前先看能不能收回来:银行家算法
总结
死锁像火灾一样:不可预测、蔓延迅速、危害大。编写并发程序的时候,一定要特别关注。
张三思评:
以前在操作系统教材里学到的知识总是过于肤浅和死记硬背, 通过代码的实际操作,自己构造死锁,则能够很好地体会死锁的本质.