一个死锁问题的分析
Posted 横刀天笑的碎碎念
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个死锁问题的分析相关的知识,希望对你有一定的参考价值。
现象描述
周五有同学发邮件说升级我们一个组件的版本后应用就起不来了,貌似是有死锁,并发了一个jstack文件。僵住的两个线程如下:
死锁原因
死锁的表面原因也很简单:上图中下面一个线程使用了netty3里的HashWheelTimer,调用了这个HashWheelTimer的start()方法,而这个方法里会await在一个CountDownLatch上,而这个CountDownLatch的countDown()方法是在这个HashWheelTimer的Worker线程的run方法调用的,这个Worker线程就是上图的第一个线程,但是现在这个线程被BLOCKED了。
所以现在这两个线程僵持在这里,HashWheelTimer的start()方法无法退出,而timer线程又被BLOCKED,无法执行countDown()。
但是在看了好几遍代码后也想不明白为什么这里会发生死锁,而且timer线程被block的地方的代码如下所示:
第95行就是调用字符串的equals方法,这个有什么好block的呢?
幸运的是现场还在,然后去机器上给jstack加m参数,又抓了一次(加m参数不仅会打印出java栈,还会打印native frame)。
然后根据上面的native frame名字去jvm源码里搜索一下,找到systemDictionary.cpp里相关方法:
发现在这个地方某些情况会对class_loader加锁。嗯?这里的class loader是什么呢?正是WebappClassLoader,而在之前我好像在哪里看到了某个线程里对WebappClassLoader已经加锁了。回头看文章开头的第一个图,会发现调用HashWheelTimer.start()方法的线程在之前已经获得了WebappClassLoader的锁了。
我们再回头去分析,是谁在WebappClassLoader上加锁的。原来SpringConfiguration这个类被标记了@Configuration,这样spring在初始化的时候就会使用cglib的字节码增强(AbstractClassGenerator):
这里对WebappClassLoader加锁了,然后调用到的ReflectUtils.defineClass方法还调用了:
// Force static initializers to run.
Class.forName(className, true, loader);
注意第二个参数,这个参数为true的时候会执行类的初始化工作,也就是类里的static block会执行。而SpringConfiguration这个类恰好有一个static block,然后在后续的调用链里很不幸的调用了HashWheelTimer的start方法,进而造成了死锁。
场景复现
那么上面其实是根据代码的一些分析,那么我们是不是能用一个简化的例子复现呢:锁住class loader,然后构造出一个类似HashWheelTimer使用CountDownLatch来。
Ok,经过前面的分析,那其实就是只要我们将class loader锁住,然后在锁里去执行一个类似HashWheelTimer的结构就可以复现了,我简单的写了几个类:
简单的介绍下几个类:Deadlock就是一个main方法,LockClassLoader里会对class loader加锁,而TestCountDown和Worker两个类就类似HashWheelTimer里的那个CountDownLatch逻辑。
激动,就要复现了。编译,执行...,程序直接执行完毕退出了,并没有卡住。这是怎么回事呢?原来在systemDictionary.cpp里还有一个判断的:is_parallelCapable,如果为true的话是不加锁的。我们来看看这个的代码:
所以是不是加锁,还是有几个条件的。我们现在不关心其他条件,只关心class_loader.is_null()。这个说的是如果你的class loader是系统默认的,则不用加锁。而回到我们文章开头的例子,我们的class loader是WebappClassLoader(tomcat定义的)。
所以要复现这个例子,我们还要写个简单的ClassLoader。然后我们将Deadlock的代码改写为:
再次执行。哇咔咔,这次应用block住了,上jstack:
这个栈,基本上和文章开头的栈类似:19行,我们在MyClassLoader上加锁了。
所以现在情况比较明朗了,死锁的最根本原因是cglib里对class loader加锁了。
总结
使用锁的范围越小越好。cglib里将类的初始化放到了锁里,这个锁的范围太大了,如果有人特别喜欢写static block,可能一不小心整个应用初始化都在锁范围内。
不要使用public的东西作为锁对象。cglib里使用class loader作为锁,而这个东西几乎是整个应用范围内都可见的,很容易造成死锁。
不要在spring的bean里还写static block。本文的问题,如果没有在标记为@Configuration的类里写一个static block是不会导致问题的,这个千万要注意。
以上是关于一个死锁问题的分析的主要内容,如果未能解决你的问题,请参考以下文章