最近看一些Java内存模型方面的书,讲了一下Java的对象的内存分配过程,其中有个例子讲解多线程锁的问题,说了下面的例子:
单例写法 双重校验写法
//------------------------双重校验锁------------------
private static Singleton singleton2;//-------1
public static Singleton getInstance4() {//------------2
if (singleton2 == null) {//-----------------------3
synchronized (Singleton.class) {//------------4
if (singleton2 == null)//-----------------5
singleton2 = new Singleton();//-------6
}
}
return singleton;
}
问题处在了第6步,Java创建对象的第6步可以分为以下三步:
memory = allocate();//----1
ctorInstance(memory);//-2
instance = memory;//-----3
其中2,3步在JVM编译优化时可能发生重排序,这和采用的JIT有关,并且该重排序遵循intra-thread semantics法则(重排序后不会影响单线程的执行结果)。
如果发生重排序,第3步先于第2步执行,那么A线程可能只是让对象指向内存地址,并没有实质的初始化对象,那么线程B调用时就会发生错误。
解决方案
-
采用volatile
在Java1.5以后,volatile关键字被加强,这种重排序不允许在多线程中发生。
即在对象声明加上volatile关键字。
实质:禁止编译的重排序。
-
采用JVM初始化类时加锁
JVM的类的初始化阶段,会获取锁,该锁可以同步多线程对一个类的初始化。
此时衍生一种称为:Initialization On Demand Holder idiom的解决方案。
//-----------------------------静态内部类---------------
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance3() {
return SingletonHolder.INSTANCE;
}
** 实质: **利用JVM的多线程初始化对象的特性,允许重排序,但对其他线程不可见。
另外根据Java语言规范,一个类在一下5种情况会发生初始化:
- T是一个类,并且T的实例被创建。
- T是一个类,且T中的静态方法被调用
- T是一个类,且T中的一个静态字段被赋值。
- T是一个类,且T中的非常量字段被使用
- T是一个顶级类(TOP Level Class),有断言语句嵌套在T内部被执行。(assert语句,很少用改规则)