ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] ### 死锁定义 首先我们先来看看死锁的定义:“死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。”那么我们换一个更加规范的定义:“集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。” 竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西。 ## 产生死锁的四个必要条件: (1) 互斥条件:一个资源每次只能被一个进程使用。 (2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 (3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。 (4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。 ## 死锁的解除与预防: 理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态 的情况下占用资源。因此,对资源的分配要给予合理的规划。 ### 举个栗子 上面的内容可能有些抽象,因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示: ![死锁](https://user-gold-cdn.xitu.io/2018/3/19/1623d495a36b4c2c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 我们用一段代码来模拟上述过程: ~~~ public static void main(String[] args) { final Object a = new Object(); final Object b = new Object(); Thread threadA = new Thread(new Runnable() { public void run() { synchronized (a) { try { System.out.println("now i in threadA-locka"); Thread.sleep(1000l); synchronized (b) { System.out.println("now i in threadA-lockb"); } } catch (Exception e) { // ignore } } } }); Thread threadB = new Thread(new Runnable() { public void run() { synchronized (b) { try { System.out.println("now i in threadB-lockb"); Thread.sleep(1000l); synchronized (a) { System.out.println("now i in threadB-locka"); } } catch (Exception e) { // ignore } } } }); threadA.start(); threadB.start(); } 复制代码 ~~~ 程序执行结果如下: ![程序执行结果](https://user-gold-cdn.xitu.io/2018/3/19/1623d495a356a7c7?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 很明显,程序执行停滞了。 ### 死锁检测 在这里,我将介绍两种死锁检测工具 #### 1、Jstack命令 jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。**线程快照**是当前java虚拟机内每一条线程**正在执行**的**方法堆栈**的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如`线程间死锁`、`死循环`、`请求外部资源导致的长时间等待`等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 首先,我们通过jps确定当前执行任务的进程号: ~~~ jonny@~$ jps 597 1370 JConsole 1362 AppMain 1421 Jps 1361 Launcher 复制代码 ~~~ 可以确定任务进程号是1362,然后执行jstack命令查看当前进程堆栈信息: ~~~ jonny@~$ jstack -F 1362 Attaching to process ID 1362, please wait... Debugger attached successfully. Server compiler detected. JVM version is 23.21-b01 Deadlock Detection: Found one Java-level deadlock: ============================= "Thread-1": waiting to lock Monitor@0x00007fea1900f6b8 (Object@0x00000007efa684c8, a java/lang/Object), which is held by "Thread-0" "Thread-0": waiting to lock Monitor@0x00007fea1900ceb0 (Object@0x00000007efa684d8, a java/lang/Object), which is held by "Thread-1" Found a total of 1 deadlock. 复制代码 ~~~ 可以看到,进程的确存在死锁,两个线程分别在等待对方持有的Object对象 #### 2、JConsole工具 Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。 我们在命令行中敲入jconsole命令,会自动弹出以下对话框,选择进程1362,并点击“**链接**” ![新建连接](https://user-gold-cdn.xitu.io/2018/3/19/1623d495a37b40a7?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 进入所检测的进程后,选择“线程”选项卡,并点击“检测死锁” ![检测死锁](https://user-gold-cdn.xitu.io/2018/3/19/1623d495a36554b7?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 可以看到以下画面: ![死锁检测结果](https://user-gold-cdn.xitu.io/2018/3/19/1623d495a56c849f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 可以看到进程中存在死锁。 以上例子我都是用synchronized关键词实现的死锁,如果读者用ReentrantLock制造一次死锁,再次使用死锁检测工具,也同样能检测到死锁,不过显示的信息将会更加丰富,有兴趣的读者可以自己尝试一下。 ### 死锁预防 如果一个线程每次只能获得一个锁,那么就不会产生锁顺序的死锁。虽然不算非常现实,但是也非常正确(一个问题的最好解决办法就是,这个问题恰好不会出现)。不过关于死锁的预防,这里有以下几种方案: #### 1、以确定的顺序获得锁 如果必须获取多个锁,那么在设计的时候需要充分考虑不同线程之前获得锁的顺序。按照上面的例子,两个线程获得锁的时序图如下: ![时序图](https://user-gold-cdn.xitu.io/2018/3/19/1623d495aa379fe7?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 如果此时把获得锁的时序改成: ![新时序图](https://user-gold-cdn.xitu.io/2018/3/19/1623d495c63027dc?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 那么死锁就永远不会发生。 针对两个特定的锁,开发者可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,那么死锁也不会发生。 ![哲学家进餐](https://user-gold-cdn.xitu.io/2018/3/19/1623d495c647d5c4?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 问题变得更加复杂一些,如果此时有多个线程,都在竞争不同的锁,简单按照锁对象的hashCode进行排序(单纯按照hashCode顺序排序会出现“环路等待”),可能就无法满足要求了,这个时候开发者可以使用**银行家算法**,所有的锁都按照特定的顺序获取,同样可以防止死锁的发生,该算法在这里就不再赘述了,有兴趣的可以自行了解一下。 #### 2、超时放弃 当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了`boolean tryLock(long time, TimeUnit unit) throws InterruptedException`方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。 还是按照之前的例子,时序图如下: ![时序图](https://user-gold-cdn.xitu.io/2018/3/19/1623d495c8474263?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) ### 其他形式的死锁 我们再来回顾一下死锁的定义,“死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。” 死锁条件里面的竞争资源,可以是线程池里的线程、网络连接池的连接,数据库中数据引擎提供的锁,等等一切可以被称作竞争资源的东西。 #### 1、线程池死锁 用个例子来看看这个死锁的特征: ~~~ final ExecutorService executorService = Executors.newSingleThreadExecutor(); Future<Long> f1 = executorService.submit(new Callable<Long>() { public Long call() throws Exception { System.out.println("start f1"); Thread.sleep(1000);//延时 Future<Long> f2 = executorService.submit(new Callable<Long>() { public Long call() throws Exception { System.out.println("start f2"); return -1L; } }); System.out.println("result" + f2.get()); System.out.println("end f1"); return -1L; } }); 复制代码 ~~~ 在这个例子中,线程池的任务1依赖任务2的执行结果,但是线程池是单线程的,也就是说任务1不执行完,任务2永远得不到执行,那么因此造成了死锁。原因图解如下: ![线程池死锁](https://user-gold-cdn.xitu.io/2018/3/19/1623d495d05cfc0b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 执行jstack命令,可以看到如下内容: ~~~ "pool-1-thread-1" prio=5 tid=0x00007ff4c10bf800 nid=0x3b03 waiting on condition [0x000000011628c000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000007ea51cf40> (a java.util.concurrent.FutureTask$Sync) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186) at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:834) at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:994) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1303) at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:248) at java.util.concurrent.FutureTask.get(FutureTask.java:111) at com.test.TestMain$1.call(TestMain.java:49) at com.test.TestMain$1.call(TestMain.java:37) at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334) at java.util.concurrent.FutureTask.run(FutureTask.java:166) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:722) 复制代码 ~~~ 可以看到当前线程wait在java.util.concurrent.FutureTask对象上。 解决办法:扩大线程池线程数 or 任务结果之间不再互相依赖。 #### 2、网络连接池死锁 同样的,在网络连接池也会发生死锁,假设此时有两个线程A和B,两个数据库连接池N1和N2,连接池大小都只有1,如果线程A按照先N1后N2的顺序获得网络连接,而线程B按照先N2后N1的顺序获得网络连接,并且两个线程在完成执行之前都不释放自己已经持有的链接,因此也造成了死锁。 ~~~ // 连接1 final MultiThreadedHttpConnectionManager connectionManager1 = new MultiThreadedHttpConnectionManager(); final HttpClient httpClient1 = new HttpClient(connectionManager1); httpClient1.getHttpConnectionManager().getParams().setMaxTotalConnections(1); //设置整个连接池最大连接数 // 连接2 final MultiThreadedHttpConnectionManager connectionManager2 = new MultiThreadedHttpConnectionManager(); final HttpClient httpClient2 = new HttpClient(connectionManager2); httpClient2.getHttpConnectionManager().getParams().setMaxTotalConnections(1); //设置整个连接池最大连接数 ExecutorService executorService = Executors.newFixedThreadPool(2); executorService.submit(new Runnable() { public void run() { try { PostMethod httpost = new PostMethod("http://www.baidu.com"); System.out.println(">>>> Thread A execute 1 >>>>"); httpClient1.executeMethod(httpost); Thread.sleep(5000l); System.out.println(">>>> Thread A execute 2 >>>>"); httpClient2.executeMethod(httpost); System.out.println(">>>> End Thread A>>>>"); } catch (Exception e) { // ignore } } }); executorService.submit(new Runnable() { public void run() { try { PostMethod httpost = new PostMethod("http://www.baidu.com"); System.out.println(">>>> Thread B execute 2 >>>>"); httpClient2.executeMethod(httpost); Thread.sleep(5000l); System.out.println(">>>> Thread B execute 1 >>>>"); httpClient1.executeMethod(httpost); System.out.println(">>>> End Thread B>>>>"); } catch (Exception e) { // ignore } } }); 复制代码 ~~~ 整个过程图解如下: ![连接池死锁](https://user-gold-cdn.xitu.io/2018/3/19/1623d495d24acafa?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 在死锁产生后,我们用jstack工具查看一下当前线程堆栈信息,可以看到如下内容: ~~~ "pool-1-thread-2" prio=5 tid=0x00007faa7909e800 nid=0x3b03 in Object.wait() [0x0000000111e5d000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000007ea73f498> (a org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$ConnectionPool) at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager.doGetConnection(MultiThreadedHttpConnectionManager.java:518) - locked <0x00000007ea73f498> (a org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$ConnectionPool) at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager.getConnectionWithTimeout(MultiThreadedHttpConnectionManager.java:416) at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:153) at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397) at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:323) at com.test.TestMain$2.run(TestMain.java:79) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334) at java.util.concurrent.FutureTask.run(FutureTask.java:166) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:722) "pool-1-thread-1" prio=5 tid=0x00007faa7a039800 nid=0x3a03 in Object.wait() [0x0000000111d5a000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000007ea73e0d0> (a org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$ConnectionPool) at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager.doGetConnection(MultiThreadedHttpConnectionManager.java:518) - locked <0x00000007ea73e0d0> (a org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$ConnectionPool) at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager.getConnectionWithTimeout(MultiThreadedHttpConnectionManager.java:416) at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:153) at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397) at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:323) at com.test.TestMain$1.run(TestMain.java:61) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334) at java.util.concurrent.FutureTask.run(FutureTask.java:166) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:722) 复制代码 ~~~ 当然,我们在这里只是一些极端情况的假定,假如线程在使用完连接池之后很快就归还,在归还连接数后才占用下一个连接池,那么死锁也就不会发生。 参考链接:《死锁终极篇》https://juejin.im/post/5aaf6ee76fb9a028d3753534