对象部分初始化:原理以及验证代码(双重检查锁与volatile相关)
对象部分初始化:原理以及验证代码(双重检查锁与volatile相关)
对象部分初始化被称为 Partially initialized objects / Partially constructed objects / Incompletely initialized objects
这三种不同的说法描述的是同一种情况,即指令重排序(reorder)导致未完全初始化的对象被使用,这会导致某些错误的发生。
文章纯原创,转载请表明地址
对象初始化过程
要理解对象部分初始化,那就要先理解对象初始化。
package Singleton;
public class NewObject {
public static void main(String[] args) {
NewObject newObject = new NewObject();
}
}
上面是一个非常简单的新建对象代码,newObject字段指向堆中新建立的对象,将上面代码反编译成字节码。
0 new #2 <Singleton/NewObject>
3 dup
4 invokespecial #3 <Singleton/NewObject.<init>>
7 astore_1
8 return
阅读字节码
1. new
根据Oracle官方文档描述,第0行(以行前标记为准) 的new
指令进行了如下操作
Memory for a new instance of that class is allocated from the garbage-collected heap, and the instance variables of the new object are initialized to their default initial values (§2.3, §2.4). The objectref, a
reference
to the instance, is pushed onto the operand stack.
翻译一下就是,该指令为指定类的实例在堆中分配了内存空间,并且将这个新对象的实例变量进行了默认初始化,即 int
类型为 0, boolean
类型为 false。并且该指令还将一个指向该实例的引用推入操作数栈中。
而dup
复制一份操作数栈顶的值,并且推入栈中 。
2. invokespecial
这个指令比较复杂,此处只需要知道该指令在此处调用了对象的初始化函数 NewObject.<init>
,对象初始化会按照静态变量、静态初始化块->变量、初始化块->构造器等顺序进行初始化,这个不是关键,关键是初始化在此时进行。该指令结束后对象会被正确的初始化。
3. astore
该指令将操作数栈顶的值储存到局部变量表中,astore_1
在此处代表的就是将值储存到变量newObjec
t中。
如果变量不是声明在方法中,而是声明在类中,那指令会变为putfield
。无论变量声明在何处,使用哪个指令,目的是为了将操作数栈顶的值储存到它该去的地方。
指令重排下的对象初始化
初始化的过程看起来没有任何问题,按照123的顺序执行的话在使用对象引用时对象一定是初始化完成的,但是为了效率,当今的CPU是”流水线“执行指令,即指令顺序输入,乱序执行,CPU在确保最终结果的前提下会按照最高效率的方式执行指令,而不是顺序的执行。
在对象初始化的过程中,CPU很可能的执行顺序是132,即 new
astore
invokespecial
。
如果是在单线程的情况下,132的执行顺序不会造成什么问题,因为CPU会保证不在invokespecial
完成前使用对象。
但是在多线程的情况下就不一样了,乱序执行会导致线程A在对象初始化完成前就将引用X指向了堆中的对象,这个引用X是共享资源,其他线程也能看的到这个变量。线程B并不知道线程A中发生了什么,当线程B需要使用引用X的时候会出现以下三种情况
- 线程A还未将引用X指向对象,线程B获得的X是null;
- 初始化完成,线程B使用的对象是正确的对象;
- 引用X指向了堆中的对象,但是线程A中进行的初始化未完成,线程B使用的对象是部分初始化的对象。
Show me the code
对象部分初始化的问题最开始是在学习单例设计模式、双重检查锁(Double-check-lock)的过程中了解到的,DCL由于指令重排序,不在对象上加volatile
关键字就会导致对象部分初始化问题。原理问题在国内外各种博客和论坛上都有描述,也都大同小异。
但困扰我的关键在于没有找到能给出DCL不加volatile会出问题的代码,换句话说,大家谈的都是理论,没有博客/文章/回答能够用代码说明这个问题确实存在。
根据维基百科的描述,这个问题是非常难以再现的。
Depending on the compiler, the interleaving of threads by the scheduler and the nature of other concurrent system activity, failures resulting from an incorrect implementation of double-checked locking may only occur intermittently. Reproducing the failures can be difficult.
在我尝试亲手复现错误的代码时,我发现如果要把测试放在单例类中,则一次运行时只能对对象进行一次初始化,其他线程只有在这一次初始化的间隙中有机会调用“不正确”的对象,在这种情况下我可能手动把程序跑上三天三夜都没办法复现一次这个问题。
于是换了一个思路,并不需要在DCL的单例模式中证明这个问题,只要能证明对象部分初始化问题存在即可。
代码设计思路:
- 乱序重排发生在对象初始化中,需要有一个线程尽可能多的进行类的初始化,好让其他线程能尽量捕捉到问题(static class Initialize)
- 需要许多个线程不断的调用被初始化的类,并且判断这个类是否有被正确初始化(static class GetObject)
- 存在一个类作为被初始化的对象(class PartiallyInitializedObject)
- 存在一个类持有上面对象的引用,线程通过这个类进行对象初始化并且给引用赋值,也通过这个类获取到引用(class Builder)
代码
mport java.util.Random;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
class PartiallyInitializedObject{
static long counter;
// final field will avoid partiallyInitializedObject
// final long id = counter++;
public int n;
public PartiallyInitializedObject(int n){
this.n = n;
}
}
class Builder{
public int createNumber = 0;
public AtomicInteger getNumber = new AtomicInteger(0);
Random rand = new Random(47);
//private volatile PartiallyInitializedObject partiallyInitializedObject;
private PartiallyInitializedObject partiallyInitializedObject;
public PartiallyInitializedObject get(){
getNumber.incrementAndGet();
return partiallyInitializedObject;
}
public void initialize(){
partiallyInitializedObject = new PartiallyInitializedObject(rand.nextInt(20)+5);
createNumber++;
}
}
public class PartiallyInitialized {
static class Initialize implements Runnable{
Builder builder;
public Initialize(Builder builder){
this.builder = builder;
}
@Override
public void run() {
while(!Thread.interrupted()){
builder.initialize();
}
}
}
static class GetObject implements Runnable{
static int count =0;
final int id = count++;
CyclicBarrier cyclicBarrier;
Builder builder;
public GetObject(CyclicBarrier c, Builder builder){
cyclicBarrier = c;
this.builder = builder;
}
@Override
public void run() {
while (!Thread.interrupted()) {
PartiallyInitializedObject p = builder.get();
if (p.n == 0) {
System.out.println("Thread " + id +" Find Partially Initialized Object " + p.n);
Thread.currentThread().interrupt();
}
}
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("Thread " + id +" Interrupted");
}
}
public static void main(String[] args) throws BrokenBarrierException, InterruptedException{
// first initialize(), second get()
// 1 initialize(), 9 get()
Builder builder = new Builder();
CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
ExecutorService exec = Executors.newFixedThreadPool(10);
exec.execute(new Initialize(builder));
for(int i=0; i<9; i++){
exec.execute(new GetObject(cyclicBarrier, builder));
}
// exec.execute(new Initialize(builder));
try {
cyclicBarrier.await(3, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("No Partially Initialized Object Found");
}
exec.shutdownNow();
System.out.println("Builder create "+builder.createNumber +" Object And Try to get "+ builder.getNumber.get()+ " times");
}
}
Builder
类中的变量partiallyInitializedObject
不使用volatile
修饰时输出如下
Thread 5 Find Partially Initialized Object 13
Thread 3 Find Partially Initialized Object 23
Thread 0 Find Partially Initialized Object 6
Thread 1 Find Partially Initialized Object 10
Thread 2 Find Partially Initialized Object 11
Thread 8 Find Partially Initialized Object 23
Thread 4 Find Partially Initialized Object 14
Thread 6 Find Partially Initialized Object 6
Thread 7 Find Partially Initialized Object 24
Thread 7 Interrupted
Thread 5 Interrupted
Thread 3 Interrupted
Thread 8 Interrupted
Thread 0 Interrupted
Thread 6 Interrupted
Thread 4 Interrupted
Thread 2 Interrupted
Thread 1 Interrupted
Builder create 46736 Object And Try to get 231239 times
Builder
类中的变量partiallyInitializedObject
使用volatile
修饰时输出如下
No Partially Initialized Object Found
Builder create 7661170 Object And Try to get 72479637 times
Thread 3 Interrupted
Thread 7 Interrupted
Thread 0 Interrupted
Thread 6 Interrupted
Thread 1 Interrupted
Thread 8 Interrupted
Thread 5 Interrupted
Thread 2 Interrupted
Thread 4 Interrupted
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at Singleton.PartiallyInitialized$GetObject.run(PartiallyInitialized.java:66)
......
代码中在线程池在执行调用GetObject
线程之前先执行Initialize
的线程,如果把exec.execute(new Initialize(builder));
放到GetObject
的线程后面,那就会出现之前说的三种情况中的第一种:GetObject
获得的引用为空。
观察代码和输出,在GetObject
线程中,只有当对象PartiallyInitializedObject.n
的值为0时才会进行输出并且打断当前线程,而在Builder
的initialize()
中能很明显的看到,对象的n值是大于等于5并且小于25,即永远不可能为0。但输出的结果却证明了GetObject
线程在某些时刻确实能得到为0的n值。代码剩余的细节这里就不再赘述。
到这一步就能够说明确实存在指令重排序而导致的对象部分初始化问题,由于synchronized
和volatile
保证可见性和有序性的原理并不相同,所以在DCL单例模式这种特殊的情况下,synchronized
也不能很好的确保正确。当然,由于种种原因,DCL单例模式已经基本被弃用了,这篇文章只做一些相关的探讨。
参考
https://stackoverflow.com/questions/7855700/why-is-volatile-used-in-double-checked-locking
https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.new