Java面试必问-死锁终极篇

背景

这个话题是源自笔者以前跟人的一次技术讨论,“你是怎么发现死锁的并且是如何预防、如何解决的?”以前听到的这个问题的时候,虽然脑海里也有一些思路,但是都是不够系统化的东西。直到最近亲身经历一次死锁,才做了这么一次集中的思路整理,撰录以下文字。希望对同样问题的同学有所帮助。

死锁定义

首先我们先来看看死锁的定义:“死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。”那么我们换一个更加规范的定义:“集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。”

竞争的资源可以是:锁、网络连接、通知事件,磁盘、带宽,以及一切可以被称作“资源”的东西。

举个栗子

上面的内容可能有些抽象,因此我们举个例子来描述,如果此时有一个线程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();
}
复制代码

程序执行结果如下:

程序执行结果

 

很明显,程序执行停滞了。

 

死锁检测

在这里,我将介绍两种死锁检测工具

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,并点击“链接

 

新建连接

 

 

进入所检测的进程后,选择“线程”选项卡,并点击“检测死锁”

检测死锁

 

可以看到以下画面:

死锁检测结果

 

可以看到进程中存在死锁。

 

以上例子我都是用synchronized关键词实现的死锁,如果读者用ReentrantLock制造一次死锁,再次使用死锁检测工具,也同样能检测到死锁,不过显示的信息将会更加丰富,有兴趣的读者可以自己尝试一下。

死锁预防

如果一个线程每次只能获得一个锁,那么就不会产生锁顺序的死锁。虽然不算非常现实,但是也非常正确(一个问题的最好解决办法就是,这个问题恰好不会出现)。不过关于死锁的预防,这里有以下几种方案:

1、以确定的顺序获得锁

如果必须获取多个锁,那么在设计的时候需要充分考虑不同线程之前获得锁的顺序。按照上面的例子,两个线程获得锁的时序图如下:

时序图

 

 

如果此时把获得锁的时序改成:

新时序图

 

那么死锁就永远不会发生。 针对两个特定的锁,开发者可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,那么死锁也不会发生。

哲学家进餐

 

 

问题变得更加复杂一些,如果此时有多个线程,都在竞争不同的锁,简单按照锁对象的hashCode进行排序(单纯按照hashCode顺序排序会出现“环路等待”),可能就无法满足要求了,这个时候开发者可以使用银行家算法,所有的锁都按照特定的顺序获取,同样可以防止死锁的发生,该算法在这里就不再赘述了,有兴趣的可以自行了解一下。

2、超时放弃

当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。 还是按照之前的例子,时序图如下:

时序图

 

 

其他形式的死锁

我们再来回顾一下死锁的定义,“死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。” 死锁条件里面的竞争资源,可以是线程池里的线程、网络连接池的连接,数据库中数据引擎提供的锁,等等一切可以被称作竞争资源的东西。

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永远得不到执行,那么因此造成了死锁。原因图解如下:

线程池死锁

 

 

执行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
        }
    }
});
复制代码

整个过程图解如下:

连接池死锁

 

 

在死锁产生后,我们用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)
复制代码

当然,我们在这里只是一些极端情况的假定,假如线程在使用完连接池之后很快就归还,在归还连接数后才占用下一个连接池,那么死锁也就不会发生。

总结

在我的理解当中,死锁就是“两个任务以不合理的顺序互相争夺资源”造成,因此为了规避死锁,应用程序需要妥善处理资源获取的顺序。 另外有些时候,死锁并不会马上在应用程序中体现出来,在通常情况下,都是应用在生产环境运行了一段时间后,才开始慢慢显现出来,在实际测试过程中,由于死锁的隐蔽性,很难在测试过程中及时发现死锁的存在,而且在生产环境中,应用出现了死锁,往往都是在应用状况最糟糕的时候——在高负载情况下。因此,开发者在开发过程中要谨慎分析每个系统资源的使用情况,合理规避死锁,另外一旦出现了死锁,也可以尝试使用本文中提到的一些工具,仔细分析,总是能找到问题所在的。


作者:江溢Jonny
链接:https://juejin.im/post/5aaf6ee76fb9a028d3753534
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

展开阅读全文

Java面试必问,ThreadLocal终极

03-29

前言rnrn在面试环节中,考察"ThreadLocal"也是面试官的家常便饭,所以对它理解透彻,是非常有必要的.rnrn有些面试官会开门见山的提问:rnrn“知道ThreadLocal吗?”rn“讲讲你对ThreadLocal的理解”rn当然了,也有面试官会慢慢引导到这个话题上,比如提问“在多线程环境下,如何防止自己的变量被其它线程篡改”,将主动权交给你自己,剩下的靠自己发挥。rnrn那么ThreadLocal可以做什么,在了解它的应用场景之前,我们先看看它的实现原理,只有知道了实现原理,才好判断它是否符合自己的业务场景。rnrnThreadLocal是什么rnrn首先,它是一个数据结构,有点像HashMap,可以保存"key : value"键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。rnrnThreadLocal localName = new ThreadLocal();rnlocalName.set("占小狼");rnString name = localName.get();rn在线程1中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值占小狼,同时在线程1中通过localName.get()可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。rnrn这是为什么,如何实现?不过之前也说了,ThreadLocal保证了各个线程的数据互不干扰。rnrn看看set(T value)和get()方法的源码rnrn public void set(T value) rn Thread t = Thread.currentThread();rn ThreadLocalMap map = getMap(t);rn if (map != null)rn map.set(this, value);rn elsern createMap(t, value);rnrnrnpublic T get() rn Thread t = Thread.currentThread();rn ThreadLocalMap map = getMap(t);rn if (map != null) rn ThreadLocalMap.Entry e = map.getEntry(this);rn if (e != null) rn @SuppressWarnings("unchecked")rn T result = (T)e.value;rn return result;rn rn rn return setInitialValue();rnrnrnThreadLocalMap getMap(Thread t) rn return t.threadLocals;rnrn可以发现,每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的threadLocals变量中,当执行set方法中,是从当前线程的threadLocals变量获取。rnrn所以在线程1中set的值,对线程2来说是摸不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。rnrn那每个线程中的ThreadLoalMap究竟是什么?rnrnThreadLoalMaprnrn本文分析的是1.7的源码。rnrn从名字上看,可以猜到它也是一个类似HashMap的数据结构,但是在ThreadLocal中,并没实现Map接口。rnrn在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。rnrnrn这里需要注意的是,ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。rnrnhash冲突rnrn没有链表结构,那发生hash冲突了怎么办?rnrn先看看ThreadLoalMap中插入一个key-value的实现rnrnprivate void set(ThreadLocal key, Object value) rn Entry[] tab = table;rn int len = tab.length;rn int i = key.threadLocalHashCode & (len-1);rnrn for (Entry e = tab[i];rn e != null;rn e = tab[i = nextIndex(i, len)]) rn ThreadLocal k = e.get();rnrn if (k == key) rn e.value = value;rn return;rn rnrn if (k == null) rn replaceStaleEntry(key, value, i);rn return;rn rn rnrn tab[i] = new Entry(key, value);rn int sz = ++size;rn if (!cleanSomeSlots(i, sz) && sz >= threshold)rn rehash();rnrn每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647。rnrn在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上;2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置;rnrn这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置rnrn可以发现,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。rnrn内存泄露rnrnThreadLocal可能导致内存泄漏,为什么?先看看Entry的实现:rnrnstatic class Entry extends WeakReference> rn /** The value associated with this ThreadLocal. */rn Object value;rnrn Entry(ThreadLocal k, Object v) rn super(k);rn value = v;rn rnrn通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。rnrn这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。rnrn如何避免内存泄露rnrn既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。rnrn如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。rnrnThreadLocal localName = new ThreadLocal();rntry rn localName.set("占小狼");rn // 其它业务逻辑rn finally rn localName.remove();rn 论坛

没有更多推荐了,返回首页