假笨说-类初始化死锁导致线程被打爆!打爆!爆!

概述

之前写过关于类加载死锁的文章,消失的死锁,说的是类加载过程中发生的死锁,我们从线程dump里完全看不出死锁的迹象,但是确实发生了死锁,没了解的建议看看我前面的那篇文章

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

我们线上的现象是发现非常多的线程都卡死在同一个地方,也不是在做类加载,如果是死循环,那cpu肯定上去了,但是cpu并没有上去,因此比较诡异

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

Demo

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

为了让问题能重现,我选择了一个最简单的办法,就是debug,一般情况下,并发导致的问题,通过debug都可以模拟出来,并发无非就是控制代码执行的先后顺序,debug显然可以做到这一点

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

这里你看下Thread状态是RUNNABLE,但是又是卡在Object.wait()处的,这里确实只能说是JVM里的一个bug吧,状态不一致,我之前在InfoQ上发过一篇文章JVM Bug:多个线程持有一把锁,解释了这个状态不一致的问题。

Object.wait是哪里调的

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

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

类的初始化过程

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

当有个线程正在执行这个类的clinit方法的时候,就会设置这个类的状态为being_initialized,当正常执行完之后就马上设置为fully_initialized,然后才唤醒其他也在等着对其做初始化的线程继续往下走,在继续走下去之前,会先判断这个类的状态,如果已经是fully_initialized了说明有线程已经执行完了clinit方法,因此不会再执行clinit方法了

当然如果执行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还是可以的

上述代码不一定能重现,不过我可以跟大家解释下可能死锁的情况,代码里我们主要定义了

  • 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特性也要谨慎,因为这会直接触发接口的初始化导致更隐蔽的循环依赖。

时间: 2025-01-20 13:37:34

假笨说-类初始化死锁导致线程被打爆!打爆!爆!的相关文章

JDK的sql设计不合理导致的驱动类初始化死锁问题

问题描述 当我们一个系统既需要mysql驱动,也需要oracle驱动的时候,在并发加载初始化这些驱动类的过程中产生死锁的可能性非常大,下面是一个模拟的例子,对于Thread2的实现其实是jdk里java.sql.DriverService的逻辑,也是我们第一次调用java.sql.DriverManager.registerDriver注册一个驱动实例要走的逻辑(jdk1.6下),不过这篇文章是使用我们生产环境的一个系统的线程dump和内存dump为基础进行分析展开的. 01 import ja

Java类初始化和实例化中的2个“雷区”_java

在考虑类初始化时,我们都知道进行子类初始化时,如果父类没有初始化要先初始化子类.然而事情并没有一句话这么简单. 首先看看Java中初始化触发的条件: (1)在使用new实例化对象,访问静态数据和方法时,也就是遇到指令:new,getstatic/putstatic和invokestatic时: (2)使用反射对类进行调用时: (3)当初始化一个类时,父类如果没有进行初始化,先触发父类的初始化: (4)执行入口main方法所在的类: (5)JDK1.7动态语言支持中方法句柄所在的类,如果没有初始化

JAVA之旅(十四)——静态同步函数的锁是class对象,多线程的单例设计模式,死锁,线程中的通讯以及通讯所带来的安全隐患,等待唤醒机制

JAVA之旅(十四)--静态同步函数的锁是class对象,多线程的单例设计模式,死锁,线程中的通讯以及通讯所带来的安全隐患,等待唤醒机制 JAVA之旅,一路有你,加油! 一.静态同步函数的锁是class对象 我们在上节验证了同步函数的锁是this,但是对于静态同步函数,你又知道多少呢? 我们做一个这样的小实验,我们给show方法加上static关键字去修饰 private static synchronized void show() { if (tick > 0) { try { Thread

Python提示[Errno 32]Broken pipe导致线程crash错误解决方法_python

本文实例讲述了Python提示[Errno 32]Broken pipe导致线程crash错误解决方法.分享给大家供大家参考.具体方法如下: 1. 错误现象ThreadingHTTPServer 实现的 http 服务,如果客户端在服务器返回前,主动断开连接,则服务器端会报 [Errno 32] Broken pipe 错,并导致处理线程 crash. 下面先看个例子,python 版本: 2.7 示例代码 复制代码 代码如下: #!/usr/bin/env python #!coding=ut

假笨说-我是如何走上JVM这条贼船的

这两年陆陆续续写了20来篇和JVM相关的文章,虽然不是很多,但是也得到了一些人的认可,贴上了JVM的标签,还是挺欣慰的,2016年算是和JVM深绑的一年,现在2016年一不留神也快过了,也赶个时髦,来个小结,不过不铺开讲其他的事,就小结下我是如何走上JVM这条路的 Flex的不解之缘 不扯远啦,从本科开始说起,大学期间虽然Java和C也有课程教过,但是基本用得不是很多,也不是很熟,何况对于我这种逃课特别严重的人呢(大一下学期开始就很少去上课了) 07年那会Flex开始流行起来了,不过还不是很广,

Java日期类SimpleDateFormat时间格式化线程安全问题

SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调试的问题,因为 DateFormat 和 SimpleDateFormat 类不都是线程安全的,在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题.下面我们通过一个具体的场景来一步步的深入学习和理解SimpleDateFormat类.     一.引子     我们都是优秀的程序员,我们都知道在程序中我们应当尽量

win7假死系类之硬盘原因造成的假死

本系统之家网站会在不定期更新win7系统假死系类,为什么会不定期更新,因为有些问题我们也不知道,需要有人像我们提问.或者小编刚刚好遇到看到之类的问题,才能找出方法解决问题,然后在网站上更新出文章告诉大家这个方法来帮大家解决问题. 1 硬盘原因造成的假死的现象: 开机的时候硬盘指示灯闪烁,有时候是硬盘指示灯不亮但是鼠标可以移动,一会后有可能硬盘指示灯就亮了.在打开复制文件的时候,经常会照成电脑卡机的现象,卡机的时间不一定,基本上跟你所处理的数据的大小有关 2 分析假死的原因: 这问题最关键的地方就

关于java类初始化顺序的问题

问题描述 关于java类初始化顺序的问题 正常来说一个类的初始化过程应该是: 1.全局静态变量 2.静态代码块 3.全局变量 4.代码块 5.构造器 有这么一个例子: public class LoadTest { //全局静态变量 static int staticI = 10; //全局变量 int i = 20; //构造器 private LoadTest() { System.out.println("staticI="+staticI); System.out.printl

c++,关于类成员函数作为线程的入口函数

问题描述 c++,关于类成员函数作为线程的入口函数 class Map {public: Bird *pB; Pig *pP; ..........}class Grav {public: Map *pM; ...... void runBird(Bird &b); void runPig(Pig &p); void run(Map &m);}void Grav::run(Map &m) { thread t[2]; t[0] = thread(&Grav::run