类初始化死锁致使线程被打爆!打爆!爆!

本文来自: PerfMa技术社区

PerfMa(笨马网络)官网java

概述

以前写过关于类加载死锁的文章,消失的死锁,说的是类加载过程当中发生的死锁,咱们从线程dump里彻底看不出死锁的迹象,可是确实发生了死锁,没了解的建议看看我前面的那篇文章sql

本文要说的是另一个问题,最近在生产环境上碰到,是类初始化致使的死锁,恩,你没看错,确实是类初始化致使的死锁,我以前写过一篇文章,不可逆的类初始化过程,这篇文章能够助你了解类的初始化过程,另外也写过一篇JDK的sql设计不合理致使的驱动类初始化死锁问题,也是关于初始化死锁的,缘由其实差很少,不过本文将这个问题描述的场景更加通用化了网络

咱们线上的现象是发现很是多的线程都卡死在同一个地方,也不是在作类加载,若是是死循环,那cpu确定上去了,可是cpu并无上去,所以比较诡异多线程

PS:有人常常给我公众号发消息咨询问题,可消息最多只能保存最近5天的,并且只能回复最近2天的,有时候忘记回了想起要回的时候就不能再回复了,若是比较紧急,问题能够发到我邮箱里,我会抽时间看这些问题并回答,不过没法保证全部的问题都会回答,由于问的人确实有点多,精力也有限。。。并发

Demo

严格意义上说,这个Demo里提到的状况是其中一个简单的场景,和咱们线上碰到的场景会有点出入,比这个会更复杂点,我后面也会提到那个场景jvm

image.png

为了让问题能重现,我选择了一个最简单的办法,就是debug,通常状况下,并发致使的问题,经过debug均可以模拟出来,并发无非就是控制代码执行的前后顺序,debug显然能够作到这一点学习

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

image.png

这里你看下Thread状态是RUNNABLE,可是又是卡在Object.wait()处的,这里确实只能说是JVM里的一个bug吧,状态不一致。优化

Object.wait是哪里调的

从线程dump的线程栈来看彻底看不出是调用了Object.wait,可是从线程输出来看确实有Object.wait,为了找出哪里调用了它,咱们能够经过jstack -m <pid>来看,看到输出以后,你会以为难以想象,确实有wait的逻辑spa

image.png

那这个逻辑从名字上来不难猜到是正在作类的初始化,那咱们先来了解下类的初始化过程

类的初始化过程

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

image.png

当有个线程正在执行这个类的clinit方法的时候,就会设置这个类的状态为being_initialized,当正常执行完以后就立刻设置为fully_initialized,而后才唤醒其余也在等着对其作初始化的线程继续往下走,在继续走下去以前,会先判断这个类的状态,若是已是fully_initialized了说明有线程已经执行完了clinit方法,所以不会再执行clinit方法了。

image.png

固然若是执行clinit失败了,那我以前那篇不可逆的类初始化过程文章就着重讲了这种状况,能够去看看。

看到这里是否能解释了咱们线上为何会有那么多线程会卡在某一个地方了?由于这个类的状态是being_initialized,因此只能等啦

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,因而也开始等待,这样就造成了互等的状况,形成了类死锁的现象。

更隐蔽的初始化死锁现象

这里提到的场景实际上是咱们线上的场景,这个状况不是很好模拟,比较难控制,固然debug jvm仍是能够的

image.png

上述代码不必定能重现,不过我能够跟你们解释下可能死锁的状况,代码里咱们主要定义了

  • Iterator接口:这个接口里有个static属性,static方法,还有个default方法,这意味着这个Iterator接口有个clinit方法,里面主要是对这个static属性赋值
  • AbstractIterator抽象类:没啥东西,就是实现Iterator接口罢了
  • Test测试类:起了两个线程,分别new了一个AbstractIterator匿名子类实例以及调用Iterator的静态方法

ok,到此我要描述一个特殊的场景了,线程1执行会建立一个AbstractIterator匿名子类实例,此时会触发AbstractIterator的初始化,同时由于其实现了Iterator接口,而Iterator接口含有defalut方法,所以这个类会被标记是一个含有default方法的类,因而在设置完AbstractIterator的类状态为being_initialized以后,会递归遍历其父接口,若是某个接口有default方法,好比Iterator,那就先触发Iterator类的初始化动做,可是在触发这个动做以前,线程2执行Iterator.empty静态方法了,因而会触发对Iterator类的初始化动做,因而设置Iterator的类状态为being_initialized,而后开始执行其clinit方法,而在clinit方法里有建立AbstractIterator匿名子类的实例,因而就会想触发AbstractIterator的初始化,可是AbstractIterator已经被线程1设置为being_initialized了,因而就只能等了,同理,线程1由于要等Iterator的初始化完成而必须等待了,从而互锁现象再次造成

相比咱们最先Demo里的场景最大的不一样是咱们看线程栈,只能看到一个线程在执行clinit方法,另一个线程并尚未在支持clinit方法,所以这个线程卡在了初始化其父接口初始化的路上了,还没拿到执行clinit的机会。

总结

类加载的死锁很隐蔽了,可是类初始化的死锁更隐蔽,因此你们要谨记在类的初始化代码里产生循环依赖,另外对于jdk8的defalut特性也要谨慎,由于这会直接触发接口的初始化致使更隐蔽的循环依赖。

一块儿来学习吧

PerfMa KO 系列课之 JVM 参数【Memory篇】

一次线上服务高 CPU 占用优化实践

相关文章
相关标签/搜索