死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。
(1)实例
死锁的本质,举个例子如果此时有一个线程 A ,按照先获持有锁 a 再获取锁 b的顺序获得锁,同时另外一个线程 B,按照先获取锁 b 再获取锁 a 的顺序获取锁。如下图所示。
image-20200708093303203.png代码模拟上述死锁过程:
import java.util.concurrent.TimeUnit;
public class DeadLock {
private static Object lockA = new Object();
private static Object lockB = new Object();
public void deadLock() {
Thread threadA = new Thread(() -> {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "尝试获取 lockB");
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取 lockB 成功");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(() -> {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取 lockB 成功");
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "尝试获取 lockA");
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
threadB.start();
}
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
deadLock.deadLock();
}
}
deadlock_02.png
(2)通过 jdk 工具 jps、jstack 排查死锁问题
1)使用 jsp 查找程序进行
jps 是 jdk 提供的一个工具,可以查看正在运行的 java 进程。
➜ ~ jps
57248 Jps
56736 Launcher
56737 DeadLock # 死锁演示进程
59618
2)使用 jstack 查看线程堆栈信息
jstack 是 jdk 提供的一个工具,可以查看 java 进程中线程堆栈信息。更详细的用法见文档最后。
➜ ~ jstack 56737
deadlock_03.png
从上可以看出死锁的数量以及死锁在代码中出现的位置。
(3)通过 jdk 提供的工具 jconsole 排查死锁问题
jconsole 是 jdk 提供的一个可视化的工具,方便排查程序的一些问题,如:程序内存溢出、死锁问题等等。更详细的用法见文档最后。
jconsole 位于 jdk 的 bin 目录中
➜ bin ./jconsole
deadlock_04.png
上图可以看到我们的程序,点击 connect。
在 jconsole 窗口中查看线程堆栈信息。
deadlock_06.png点击 DetectDeadlock 可以查看详细的死锁信息,和 jstack 展示的类似。
deadlock_08.png如何避免死锁
我们知道了死锁如何产生的,那么就知道该如何去预防。如果一个线程每次只能获取一个锁,那么就不会出现由于嵌套持有锁顺序导致的死锁。
1)正确的顺序获得锁
上面的例子出现死锁的根本原因就是获取所的顺序是乱序的,超乎我们控制的。上面例子最理想的情况就是把业务逻辑抽离出来,把获取锁的代码放在一个公共的方法里面,让这两个线程获取锁都是从我的公共的方法里面获取。
当Thread1线程进入公共方法时,获取了A锁,另外Thread2又进来了,但是A锁已经被Thread1线程获取了,所以只能阻塞等待。Thread1接着又获取锁B,Thread2线程就不能再获取不到了锁A,更别说再去获取锁B了,这样就有一定的顺序了。只有当线程1释放了所有锁,线程B才能获取。
修改后的例子:
import java.util.concurrent.TimeUnit;
public class DeadLock2 {
private static Object lockA = new Object();
private static Object lockB = new Object();
public void deadLock() {
Thread threadA = new Thread(() -> {
getLock();
});
Thread threadB = new Thread(() -> {
getLock();
});
threadA.start();
threadB.start();
}
private void getLock() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "尝试获取 lockB");
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "获取 lockB 成功");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
DeadLock2 deadLock2 = new DeadLock2();
deadLock2.deadLock();
}
}
deadlock_07.png
2)超时放弃
当线程获取锁超时了则放弃,这样就避免了出现死锁获取的情况。当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。
总结:
死锁就是“两个任务以不合理的顺序互相争夺资源”造成,因此为了规避死锁,应用程序需要妥善处理资源获取的顺序。 另外有些时候,死锁并不会马上在应用程序中体现出来,在通常情况下,都是应用在生产环境运行了一段时间后,才开始慢慢显现出来,在实际测试过程中,由于死锁的隐蔽性,很难在测试过程中及时发现死锁的存在,而且在生产环境中,应用出现了死锁,往往都是在应用状况最糟糕的时候——在高负载情况下。因此,开发者在开发过程中要谨慎分析每个系统资源的使用情况,合理规避死锁。
网友评论