一张图简单描述死锁

概述
之前写过关于类加载死锁的文章“消失的死锁”,说的是类加载过程中发生的死锁,我们从线程dump里完全看不出死锁的迹象,但是确实发生了死锁,没了解的建议看看我公众号上相关的文章。
本文要说的是另外一个问题,之前在生产环境上碰到,是类初始化导致的死锁,恩,你没看错,确实是类初始化导致的死锁,我之前写过一篇文章,不可逆的类初始化过程,这篇文章可以助你了解类的初始化过程,另外也写过一篇JDK的sql设计不合理导致的驱动类初始化死锁问题,也是关于初始化死锁的,原因其实差不多,不过本文将这个问题描述的场景更加通用化了。
Demo
严格意义上说,这个Demo里提到的情况是其中一个简单的场景,和我们线上碰到的场景会有点出入,比这个会更复杂点。

我们上面定义了A,B两个类,他们相互依赖,并且都有一个静态块,在静态块里相互调用对方的某个静态方法,我们的测试类ABTest就是用两个线程分别取调用两个类的静态方法,那我们在A和B两个类的静态块里调用对方静态方法之前设置一个断点,比如说都在System.out.println()那里设置断点,当两个线程都停到断点处的时候,我们再过掉两个断点,你会发现一个奇怪的现象,这个进程并没有退出,也就是那两个线程都没有执行完,你看到堆栈如下:

Object.wait是哪里调的
从线程dump的线程栈来看完全看不出是调用了Object.wait,但是从线程输出来看确实有Object.wait,为了找出哪里调用了它,我们可以通过jstack -m 来看,看到输出之后,你会觉得不可思议,确实有wait的逻辑

类的初始化过程
当我们第一次主动调用某个类的静态方法就会触发这个类的初始化,当然还有其他的触发情况,类的初始化说白了就是在类加载起来之后,在某个合适的时机执行这个类的clinit方法,clinit方法是什么?比如我们在类里声明一段static代码块,或者有静态属性,javac会将这些代码都统一放到一个叫做clinit的方法里,在类初始化的时候来执行这个方法,但是JVM必须要保证这个方法只能被执行一次,如果有其他线程并发调用触发了这个类的多次初始化,那只能让一个线程真正执行clinit方法,其他线程都必须等待,当clinit方法执行完之后,然后再唤醒其他等待这里的线程继续操作,当然不会再让它们有机会再执行clinit方法,因为每个类都有一个状态,这个状态可以保证这一点


Demo现象解释
我们Demo里的那两个线程,从dump来看确实是死锁了,那这个场景当时是怎么发生的呢?线程1首先执行B.test(),于是会对B类做初始化,设置B的类状态为being_initialized,接着去执行B的clinit方法,但是在clinit方法里要去调用A.test方法,理论上此时会对A做初始化并调用其test方法,但是就在设置完B的类状态之后,执行其clinit里的A.test方法之前,线程2却执行了A.test方法,此时线程2会优先负责对A的初始化工作,即设置A类的状态为being_initialized,然后再去执行A的clinit方法,此时线程1发现A的类状态是being_initialized了,那线程1就认为有线程对A类正在做初始化,于是就等待了,而线程2同样发现B的类状态也是being_initialized,于是也开始等待,这样就形成了互等的情况,造成了类死锁的现象。
总结
类加载的死锁很隐蔽了,但是类初始化的死锁更隐蔽,所以大家要谨记在类的初始化代码里产生循环依赖,另外对于jdk8的defalut特性也要谨慎,因为这会直接触发接口的初始化导致更隐蔽的循环依赖
推荐阅读