掘金江溢Jonny,转载请注明原创出处,谢谢!java
关注个人公众号,得到更多干货~ 算法
这个话题是源自笔者之前跟人的一次技术讨论,“你是怎么发现死锁的而且是如何预防、如何解决的?”之前听到的这个问题的时候,虽然脑海里也有一些思路,可是都是不够系统化的东西。直到最近亲身经历一次死锁,才作了这么一次集中的思路整理,撰录如下文字。但愿对一样问题的同窗有所帮助。数据库
首先咱们先来看看死锁的定义:“死锁是指两个或两个以上的进程在执行过程当中,因为竞争资源或者因为彼此通讯而形成的一种阻塞的现象,若无外力做用,它们都将没法推动下去。”那么咱们换一个更加规范的定义:“集合中的每个进程都在等待只能由本集合中的其余进程才能引起的事件,那么该组进程是死锁的。”apache
竞争的资源能够是:锁、网络链接、通知事件,磁盘、带宽,以及一切能够被称做“资源”的东西。bash
上面的内容可能有些抽象,所以咱们举个例子来描述,若是此时有一个线程A,按照先锁a再得到锁b的的顺序得到锁,而在此同时又有另一个线程B,按照先锁b再锁a的顺序得到锁。以下图所示: 服务器
咱们用一段代码来模拟上述过程:网络
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();
}
复制代码
程序执行结果以下: 工具
在这里,我将介绍两种死锁检测工具post
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对象
Jconsole是JDK自带的监控工具,在JDK/bin目录下能够找到。它用于链接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。并且自己占用的服务器内存很小,甚至能够说几乎不消耗。
咱们在命令行中敲入jconsole命令,会自动弹出如下对话框,选择进程1362,并点击“连接”
进入所检测的进程后,选择“线程”选项卡,并点击“检测死锁”
以上例子我都是用synchronized关键词实现的死锁,若是读者用ReentrantLock制造一次死锁,再次使用死锁检测工具,也一样能检测到死锁,不过显示的信息将会更加丰富,有兴趣的读者能够本身尝试一下。
若是一个线程每次只能得到一个锁,那么就不会产生锁顺序的死锁。虽然不算很是现实,可是也很是正确(一个问题的最好解决办法就是,这个问题刚好不会出现)。不过关于死锁的预防,这里有如下几种方案:
若是必须获取多个锁,那么在设计的时候须要充分考虑不一样线程以前得到锁的顺序。按照上面的例子,两个线程得到锁的时序图以下:
若是此时把得到锁的时序改为:
问题变得更加复杂一些,若是此时有多个线程,都在竞争不一样的锁,简单按照锁对象的hashCode进行排序(单纯按照hashCode顺序排序会出现“环路等待”),可能就没法知足要求了,这个时候开发者可使用银行家算法,全部的锁都按照特定的顺序获取,一样能够防止死锁的发生,该算法在这里就再也不赘述了,有兴趣的能够自行了解一下。
当使用synchronized关键词提供的内置锁时,只要线程没有得到锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException
方法,该方法能够按照固定时长等待锁,所以线程能够在获取锁超时之后,主动释放以前已经得到的全部的锁。经过这种方式,也能够颇有效地避免死锁。 仍是按照以前的例子,时序图以下:
咱们再来回顾一下死锁的定义,“死锁是指两个或两个以上的进程在执行过程当中,因为竞争资源或者因为彼此通讯而形成的一种阻塞的现象,若无外力做用,它们都将没法推动下去。” 死锁条件里面的竞争资源,能够是线程池里的线程、网络链接池的链接,数据库中数据引擎提供的锁,等等一切能够被称做竞争资源的东西。
用个例子来看看这个死锁的特征:
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永远得不到执行,那么所以形成了死锁。缘由图解以下:
执行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 任务结果之间再也不互相依赖。
一样的,在网络链接池也会发生死锁,假设此时有两个线程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
}
}
});
复制代码
整个过程图解以下:
在死锁产生后,咱们用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)
复制代码
固然,咱们在这里只是一些极端状况的假定,假如线程在使用完链接池以后很快就归还,在归还链接数后才占用下一个链接池,那么死锁也就不会发生。
在个人理解当中,死锁就是“两个任务以不合理的顺序互相争夺资源”形成,所以为了规避死锁,应用程序须要妥善处理资源获取的顺序。 另外有些时候,死锁并不会立刻在应用程序中体现出来,在一般状况下,都是应用在生产环境运行了一段时间后,才开始慢慢显现出来,在实际测试过程当中,因为死锁的隐蔽性,很难在测试过程当中及时发现死锁的存在,并且在生产环境中,应用出现了死锁,每每都是在应用情况最糟糕的时候——在高负载状况下。所以,开发者在开发过程当中要谨慎分析每一个系统资源的使用状况,合理规避死锁,另一旦出现了死锁,也能够尝试使用本文中提到的一些工具,仔细分析,老是能找到问题所在的。
以上就是本次写做所有内容了,若是你喜欢,欢迎关注个人公众号~ 这是给我不断写做的最大鼓励,谢谢~