死锁(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。就这样,两个线程发生了死锁。导致后续的代码都不会执行,之后的语句并不会输出。
死锁的危害非常巨大,是并发编程必须要考虑的问题。不过好在死锁的产生条件比较严苛,需要同时满足四个必要条件:
只要其中一个不满足就不可能发生死锁。
再回过头来看看上文的实例是不是满足这四个条件,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)
......
很清楚的就能看到哪几个线程发生了死锁(现在应该知道为什么要给每个线程取一个有意义的名字了)
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的顺序请求锁。
死锁的处理策略总的来说有三种方式:
对于生产环境而言,死锁的防大于治,也就是说应该将重点放在死锁的避免上。在实际工作中养成良好的习惯,可以大大减少死锁发生的概率,好的习惯总结如下:
死锁像火灾一样:不可预测、蔓延迅速、危害大。编写并发程序的时候,一定要特别关注。