类初始化导致死锁
本文来自 PerfMa社区,欢迎关注公众号
一张图简单描述死锁
如上图,Thread1 拿到了 object1,Thread2 拿到了 object2,但是现在 Thread1 需要拿到 object2 的锁才能继续往下,Thread2 又要拿到 object1 才能继续往下,于是哪个线程都无法得到满足继续往下来释放对方所需要的锁对象,从而造成了死锁。
概述
之前写过关于类加载死锁的文章“消失的死锁”,说的是类加载过程中发生的死锁,我们从线程dump里完全看不出死锁的迹象,但是确实发生了死锁,没了解的建议看看我公众号上相关的文章。
本文要说的是另外一个问题,之前在生产环境上碰到,是类初始化导致的死锁,恩,你没看错,确实是类初始化导致的死锁,我之前写过一篇文章,不可逆的类初始化过程,这篇文章可以助你了解类的初始化过程,另外也写过一篇JDK的sql设计不合理导致的驱动类初始化死锁问题,也是关于初始化死锁的,原因其实差不多,不过本文将这个问题描述的场景更加通用化了。
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,于是也开始等待,这样就形成了互等的情况,造成了类死锁的现象。
总结
类加载的死锁很隐蔽了,但是类初始化的死锁更隐蔽,所以大家要谨记在类的初始化代码里产生循环依赖,另外对于jdk8的defalut特性也要谨慎,因为这会直接触发接口的初始化导致更隐蔽的循环依赖
推荐阅读
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
如何通过 OIDC 协议实现单点登录?
什么是单点登录 我们通过一个例子来说明,假设有一所大学,内部有两个系统,一个是邮箱系统,一个是课表查询系统。现在想实现这样的效果:在邮箱系统中登录一遍,然后此时进入课表系统的网站,无需再次登录,课表网站系统直接跳转到个人课表页面,反之亦然。比较专业的定义如下: 单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。 SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 为什么要实现单点登录 单点登录的意义在于能够在不同的系统中统一账号、统一登录。用户不必在每个系统中都进行注册、登录,只需要使用一个统一的账号,登录一次,就可以访问所有系统。 通过 OIDC 协议实现单点登录 创建自己的用户目录 用户目录这个词很贴切,你的系统的总用户表就像一本书一样,书的封皮上写着“所有用户”四个字。打开第一页,就是目录,里面列满了用户的名字,翻到对应的页码就能看到这个人的邮箱,手机号,生日信息等等。无论你开发多少个应用,要确保你有一份这些应用所有用户信息的 truth source。所有的注册、认证、注销都要到你的用户目录...
- 下一篇
探秘 ThreadLocal 的实现机制与小地雷
Java 多线程类库对于共享数据的读写访问主要采用同步机制来保证线程安全,而本文所要探究的 ThreadLocal 则采用了一种完全不同的策略,它不是用来解决共享数据的并发访问问题的,ThreadLocal 让每个线程都将目标数据复制一份作为线程私有,后续对于该数据的操作都是在各自私有的副本上进行,线程之间彼此相互隔离,也就不存在竞争问题。 下面的例子演示了 ThreadLocal 的典型应用场景。在 jdk 1.8 之前,如果我们希望对日期和时间进行格式化操作,则需要使用 SimpleDateFormat 类,而我们知道它是是线程不安全的,在多线程并发执行时会出现一些奇怪的问题。对于该类使用的最佳实践则是采用 ThreadLocal 进行包装,以保证每个线程都有一份属于自己的 SimpleDateFormat 对象,如下所示: ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initial...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8安装Docker,最新的服务器搭配容器使用
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7,CentOS8安装Elasticsearch6.8.6
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS8编译安装MySQL8.0.19
- CentOS7设置SWAP分区,小内存服务器的救世主