假笨说-类初始化死锁导致线程被打爆!打爆!爆!
概述
之前写过关于类加载死锁的文章,消失的死锁,说的是类加载过程中发生的死锁,我们从线程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特性也要谨慎,因为这会直接触发接口的初始化导致更隐蔽的循环依赖。
假笨说-类初始化死锁导致线程被打爆!打爆!爆!相关推荐
- Java线上问题排障:Linux内核bug引发JVM死锁导致线程假死
Java本质上还是离不开操作系统,一来Java源码是用C/C++实现的,二来java进程还是需要依附于操作系统和硬件资源,有时候一些问题是操作系统级别导致的,下面的整个事件是源自一则真实的线上案例. ...
- 假笨说-谨防JDK8重复类定义造成的内存泄漏
概述 如今JDK8成了主流,大家都紧锣密鼓地进行着升级,享受着JDK8带来的各种便利,然而有时候升级并没有那么顺利?比如说今天要说的这个问题.我们都知道JDK8在内存模型上最大的改变是,放弃了Perm ...
- 导致线程死锁的原因?怎么解除线程死锁
欢迎大家关注我的公众号[老周聊架构],Java后端主流技术栈的原理.源码分析.架构以及各种互联网高并发.高性能.高可用的解决方案. 一.导致线程死锁的原因 多个线程同时被阻塞,它们中的一个或者全部都在 ...
- Android 线程死锁导致的ANR问题分析
最近遇到一个系统引起的ANR问题,遇到的时候特别懵,首先我取出了日志文件,由于网上分析ANR的例子比较少,无法找到参考,所以只能硬上了 在/data/anr/目录下的trace文件 ANR文件 下面是 ...
- 线程安全、volatile关键字、原子性、并发包、死锁、线程池
[线程安全.volatile关键字.原子性.并发包.死锁.线程池] 内容 线程安全 synchronized关键字\Lock接口 同步代码块 同步方法 Lock锁 高并发可见性问题 volatile关 ...
- java(八)-线程安全、volatile关键字、原子性、并发包、死锁、线程池
Day08[线程状态.volatile关键字.原子性.并发包.死锁.线程池] 今日目标 线程安全 volatile关键字 原子性 并发包 死锁 线程池 教学目标 能够说出volatile关键字的作用 ...
- java 线程假醒_Java并发基础05. 传统线程同步通信技术
先看一个问题: 有两个线程,子线程先执行10次,然后主线程执行5次,然后再切换到子线程执行10,再主线程执行5次--如此往返执行50次. 看完这个问题,很明显要用到线程间的通信了, 先分析一下思路:首 ...
- 单例设计模式-静态内部类-基于类初始化的延迟加载解决方案及原理解析
刚刚线程1看不到线程0的重排序,我们创建一个类,这个方案是使用静态内部类来解决,一会我们也会分析一下原理,我们创建一个静态内部类,静态内部类的代理模式,JVM在类的初始化阶段,也就是class被加载后 ...
- java类初始化_Java的类/实例初始化过程
昨天看到群里面有人分享了一道题目,我答错了,于是趁机了解了下Java的类/对象初始化过程: 程序的输出见文章最后 程序A主要考察的是 类实例初始化 .简单验证了下,类实例初始化过程如下:父类实例初始化 ...
最新文章
- pytorch python区别_pytorch源码解析:Python层 pytorchmodule源码
- bootstrap 中这段代码 使bundles 失败
- java 流 中文_Java IO流之中文乱码
- python的系统模块_Python操作系统模块
- ASP.NET FormsAuthentication跨站点登录时绝对地址返回的问题
- 怎样做网站,需要考虑的几个seo因素
- 点译PDF的翻译器或者PDF阅读器插件
- Feem(局域网文件传输工具)v4.3.0官方版
- ARKit入门到精通 1.0 - 实战案例 AR打地鼠-史小川-专题视频课程
- 播放量破4亿。《梦华录》创2022年国产剧豆瓣最高开分,它凭何爆火?
- JAVA新手,开始起航~~
- 蓝桥杯JAVA-4.常用数据类型
- linux挂nas盘步骤,linux下需要将nas盘挂
- 使用Remmina远程登录Ubuntu系统并实现文件共享(可实现类似Teamviewer功能)
- 【七七八八】coursera python-basis certification
- 如何配合RestTemplate优雅的记录请求以及返回的信息
- 阿里又孵出一只2000亿猛兽,马云当年的眼光太狠了
- 基于人工势场法的移动机器人路径规划研究(Matlab代码实现)
- input光标颜色修改
- [篇四章一]_在 VMWare 16 上安装 Windows 98 SE 操作系统