一。非静态域的延迟加载使用DCL实现的问题:编译器会无序写入
1.什么是无序写入:按常规先初始化,后指向。真实的情况是写指向,后初始化。
当执行instance =new Singleton(); 相当于执行了下列伪代码:
mem = allocate(); //Allocate memory for Singleton object.
instance = mem; //Note that instance is now non-null, but
//has not been initialized.
ctorSingleton(instance); //Invoke constructor for Singleton passing
//instance.
这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT 编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。
2. 优化的结果:调用构造方法实例某个域时,存在非null但没有完全初始化的状态。正确的状态应该是二种,null和非null且完全初始化。这样的结果会给程序员认为正确的DCL存在潜在的问题。
二。非静态域的延迟加载使用DCL的演变过程:
1.程序员想到了使用中间变量可以解决无序写入:
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) { //1
Singleton inst = instance; //2
if (inst == null)
{
synchronized(Singleton.class) { //3
inst = new Singleton(); //4
}
instance = inst; //5
}
}
}
return instance;
}
此代码试图避免无序写入问题。它试图通过引入局部变量 inst 和第二个 synchronized 块来解决这一问题。该理论实现如下:
1. 线程 1 进入 getInstance() 方法。
2. 由于 instance 为 null,线程 1 在 //1 处进入第一个 synchronized 块。
3. 局部变量 inst 获取 instance 的值,该值在 //2 处为 null。
4. 由于 inst 为 null,线程 1 在 //3 处进入第二个 synchronized 块。
5. 线程 1 然后开始执行 //4 处的代码,同时使 inst 为非 null,但在 Singleton 的构造函数执行前。(这就是我们刚才看到的无序写入问题。)
6. 线程 1 被线程 2 预占。
7. 线程 2 进入 getInstance() 方法。
8. 由于 instance 为 null,线程 2 试图在 //1 处进入第一个 synchronized 块。由于线程 1 目前持有此锁,线程 2 被阻断。
9. 线程 1 然后完成 //4 处的执行。
10. 线程 1 然后将一个构造完整的 Singleton 对象在 //5 处赋值给变量 instance,并退出这两个 synchronized 块。
11. 线程 1 返回 instance。
12. 然后执行线程 2 并在 //2 处将 instance 赋值给 inst。
13. 线程 2 发现 instance 为非 null,将其返回。
这里的关键行是 //5。此行应该确保 instance 只为 null 或引用一个构造完整的 Singleton 对象。该问题发生在理论和实际彼此背道而驰的情况下。
由于当前内存模型的定义,清单 7 中的代码无效。Java 语言规范(Java Language Specification,JLS)要求不能将 synchronized 块中的代码移出来。但是,并没有说不能将 synchronized 块外面的代码移入 synchronized 块中。
JIT 编译器会在这里看到一个优化的机会。此优化会删除 //4 和 //5 处的代码,组合并且生成如下代码:
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) { //1
Singleton inst = instance; //2
if (inst == null)
{
synchronized(Singleton.class) { //3
//inst = new Singleton(); //4
instance = new Singleton();
}
//instance = inst; //5
}
}
}
return instance;
}
由于编译器优化导致仍然存在无序写入的问题。
private volatile FieldType field; //注意必须使用volatile,volatile除了可见性之外,还有一个功能是避免编译器优化。
FieldType getField() {
FieldType result = field;
if (result == null) {// First check (no locking)
synchronized(this) { //这样不会象1)中synchronized整个方法,导致在初始化成功之后,访问getInstance仍然要同步的低效.
result = field; //这里使用result的原因是让result即使result由于JIT无序写入出现不为null并且没有完全初始化的情况,但是可以是能field要么为null,要么被完整初始化。此时field是null.
if (result == null){// Second check (with locking) field
//field肯定是在result完全初始化之后,再赋给field。
field = result = computeFieldValue();
}
}
return result;
}
2. 又想使用DCL,又想避免编译器优化的最后办法:利用volatile修饰域避免编译器优化。
下面的例子来自effective java,DCL的实现使用了volatile,中间变量,内部锁定。非常麻烦。
三。DCL的结论:非静态域延迟加载的推荐办法 :1.使用延迟加载,不使用DCL, 回退到损失性能的synchronized method上。 2.使用延迟加载,使用一个 static 修改字段, 使用static holder延迟加载。 3.不使用延迟加载,立即初始化。
四。神奇的volatile
volatile除了可见性,还有一个重要的功能避免编译器调优。
1.没有使用volatile:
class Test{
private boolean stop = false;
private int num = 0;
public void foo() {
num = 100;
stop = true;
//...
}
public void bar() {
if (stop)
num += num; //num=0+0,num can be 0
}
//...
}
在 JSR133中说到,一般jvm 会对其认为不会影响上下文的程序做适当优化。这其中的一个优化是:re-order. 也就是说,在上面的程序foo 函数中,两个语句的执行顺序可能被交换。这就是上面注释中num can be 0 的由来。这与volatile 有什么关系呢?事实是,如果对上面的两个变量加上volatile修饰符,上述的re-order就不会发生。
一般jvm 会对其认为不会影响上下文的程序做适当优化。这其中的一个优化是:re-order. 也就是说,在上面的程序foo 函数中,两个语句的执行顺序可能被交换。
2.使用volatile,使用的目的就是避免编译器re-order.
class test
{
private volatile boolean stop = false;
private volatile int num = 0;
public void foo()
{
num = 100; //This can happen second
stop = true; //This can happen first
//...
}
public void bar()
{
if (stop)
num += num; //num can == 0!
}
//...
}
3. 相关的文章并没有说volatile就是最终的解决DCL的办法。但是effective java是这么做的。
参http://www.ibm.com/developerworks/cn/java/j-dcl.html