一:枚举死锁 问题
在讨论上面这个问题之前,先熟悉下什么情况会触发java类的初始化。
参考jvm规范,java虚拟机实现都必须在类,接口首次被主动使用时进行初始化,那什么情况是主动使用,以下几种情形符合主动使用的要求。
- 执行以下java指令的时候,new, getstatic, putstatic, or invokestatic。也就是常说的new对象, 获得static属性,设置static属性,和调用static方法。
- 第一次MethodHandle实例的调用,相关指令为REF_getStatic,REF_putStatic,REF_invokeStatic。
- 调用类,库中的某些特定方法,比如类Class 或者包 java.lang.reflect中的某些反射方法。
- 初始化 子类的时候,会首先初始化父类
- 当虚拟机启动的某个被标明为启动类的类(即含有main()方法的类)
参考http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.5 ,jvm规范同时要求java虚拟机实现必须确保初始化过程被正确的同步,如果多个线程同时初始化一个类,仅仅允许其中的一个线程执行初始化,其他的线程必须等待,当初始化完成后,通知其他等待的线程完成初始化。
死锁代码为
- 线程A 先执行populateNames 方法,线程B进行enumClass 的初始化
- 线程A在方法内部,获取enumClass静态属性值,触发类的初始化,因为线程B正在初始化当前类,因此线程A会等待线程B初始化的完成(多线程初始化类时,只能有一个线程初始化,其他线程等待这个线程初始化的完成)
- 线程B在初始化的过程 中,会调用populateNames 方法,由于populateNames 方法内的对象锁,已经被线程A拿到,线程B拿不到此对象锁,因为会等待线程A对该锁的释放。
- 以上可以看到线程A等待线程B的初始化,而线程B等待线程A执行完populateNames 方法,造成死锁。
解决方案是将populateNames 方法中的synchronized给去掉,这个解决方案可以继续讨论。在跟踪这个代码的时候,发现这个类型的死锁问题这个包之前也出现过,看下EnumUtil.getEnumType 的代码
可以看到 之前也出现了 这个问题。
类似的问题在这两篇文章中也有说明,Spring 3.1.4之前版本中一个deadlock bug,诡异的http请求返回499的case排查
和枚举问题相似 下面这两段代码也有问题,在看下面的分析之前,可以自己分析下到底什么问题。
这两段代码会造成执行等待(不能完成正常的执行流程),代码的执行过程大体相同
1、首先主线程M获取初始化锁,设置一个标志位表示初始化正在进行。
2、首先完成对静态变量的初始化,然后进入static块,启动另一个线程N。
3、在线程N中会调用类的静态变量,这个时候看到那个标志位(还没有初始化完成),就会等待主线程初始化完成
4、由于主线程M的初始化包含 静态代码块的初始化,因此主线程的初始化会等待静态代码块的初始化完成(而代码中的while循环,和join方法都是要保证线程N的执行完成,才会向下执行)
这种问题的排查 可、可以使用jstack看下,很容易找到问题所在,当然真正的故障排查的困难在于确认是死锁的问题,咱们现在是在倒果为因的排查,简单了些。以StaticThreadInit1为例
"Thread-0" prio=6 tid=0x02530400 nid=0x1980 in Object.wait() [0x04b8f000]
java.lang.Thread.State: RUNNABLE
at StaticThreadInit1$1.run(StaticThreadInit1.java:7)
Locked ownable synchronizers:
- None
"main" prio=6 tid=0x002b9400 nid=0x17a8 runnable [0x0025f000]
java.lang.Thread.State: RUNNABLE
at StaticThreadInit1.<clinit>(StaticThreadInit1.java:10)
Locked ownable synchronizers:
- None
可以看到 另起的线程,名称为Thread-0 被锁定,而main 也被同样的锁锁住,并且一直在代码中的第10行执行,第10行代码就是 while循环。因此不能正常执行。
二:java的初始化
上面的枚举问题隐含着两个方面的内容
一:多个线程对同一个java类,接口执行初始化操作的时候,会进行同步,只有一个线程得到初始化的机会,其他线程只能进行等待,这正好是synchronized的使用场景,也在上面的枚举死锁问题得到了验证
二:静态属性值得获取会触发java初始化,同时不能正常执行的两个例子告诉我们 初始化的时候包含静态属性和静态代码块的初始化,那java的初始化到底包含哪些方面的内容呢?java的初始化又是按照什么顺序进行的初始化呢?下面说明这个问题。
上图展示了类的生命周期流向,本文仅仅看类的初始化和对象初始化两个阶段
类初始化,它是一个类或接口被首次使用的前阶段中的最后一项工作,本阶段负责为类变量赋予正确的初始值。Java 编译器把所有的类变量初始化语句和类型的静态初始化器通通收集到 <clinit> 方法内,该方法只能被 Jvm 调用,专门承担初始化工作。
对象初始化,Java 编译器在编译每个类时都会为该类至少生成一个实例初始化方法--即 "<init>()" 方法。此方法与源代码中的每个构造方法相对应,如果类没有明确地声明任何构造方法,编译器则为该类生成一个默认的无参构造方法,这个默认的构造器仅仅调用父类的无参构造器,与此同时也会生成一个与默认构造方法对应的 "<init>()" 方法.
上面还有一个阶段比较重要,就是准备阶段,java虚拟机装载了一个类文件,并做了一些相应的验证之后,就进入了准备阶段,在准备阶段,java虚拟机会为类变量分配内存,设置默认初始值,但在到达初始化阶段之前,这个值不是真正的初始值,仅仅是根据变量类型设置为默认值,比如 int 0 long 0L char ‘、u0000’,对象设置为null,boolean设置为为false。
类初始化有两个步骤
1)先确认是否存在父类,如果父类没有被初始化,先初始化父类。
2)类中是否有类初始化方法,如果有,调用此方法进行初始化。
<clinit> 并不会显示的调用父类的<clinit> 方法,而仅仅是通过java虚拟机保证在执行子类的这个方法之前,父类的<clinit> 方法已经被执行。也就是说子类的静态初始化语句一定在父类的静态初始化语句之后调用
对象初始化,init方法可能包含三个部分的代码
1)一个父类<init>方法的调用
2)实例变量初始化的代码
3)构造方法体中的代码
上面这些太枯燥了,下面看下具体的代码。
上面的代码执行下,然后分析下背后的原因。
如果有不明白的,可以看下 下面几篇文章,结合我上面的说明,应该就差不多了。
java类的初始化顺序,java对象初始化顺序,最权威的JLS初始化说明。
最后:
上面的枚举问题 简单一点就是一个牵涉到java初始化的 死锁问题。前文简单分析了下死锁的原因,同时将java初始化进行了一次的梳理。