检查我很久以前在 StackOverflow 上给出的这个答案,以了解 DCL 失败的原因以及如何修复它。
问题不在于同步/异步。问题是所谓的重新排序。
JVM 规范定义了一种称为发生前关系的东西。在单个线程中,如果语句 S1 出现在语句 S2 之前,则S1 发生在 S2 之前,即 S1 对内存所做的任何修改对 S2 都是可见的。请注意,它并不是说语句 S1 必须在 S2 之前执行。它只是说事情应该看起来好像S1 在 S2 之前执行。例如,考虑以下代码:
int x = 0;
int y = 0;
int z = 0;
x++;
y++;
z++;
z += x + y;
System.out.println(z);
在这里,JVM 执行三个增量语句的顺序无关紧要。唯一的保证是,在运行时z += x + y
,x、y 和 z 的值必须全为 1。事实上,如果重新排序不违反happens-before关系,JVM 实际上是允许对语句重新排序的。这样做的原因是有时稍微重新排序可以优化您的代码,并获得更好的性能。
缺点是允许 JVM 以一种在您使用多个线程时可能导致非常奇怪的结果的方式对事物进行重新排序。例如:
class Broken {
private int value;
private boolean initialized = false;
public void init() {
value = 5;
initialized = true;
}
public boolean isInitialized() { return initialized; }
public int getValue() { return value; }
}
假设一个线程正在执行这段代码:
while (!broken.isInitialized()) {
Thread.sleep(1); // patiently wait...
}
System.out.println(broken.getValue());
假设,现在,另一个线程在同一个Broken
实例上,
broken.init();
允许 JVM 重新排序init()
方法内的代码,首先运行initialized = true
,然后才将 设置value
为 5。如果发生这种情况,第一个线程,即等待初始化的线程,可能会打印 0!要修复,要么添加synchronized
到这两种方法,要么添加volatile
到initialized
字段。
回到 DCL,单例的初始化可能以不同的顺序执行。例如:
1. mem = allocateMem() : allocate memory for Test and save it's address.
2. construct(mem) : construct the class Test
3. t = mem : point t to the mem
可能变成:
1. mem = allocateMem() : allocate memory for Test and save it's address.
2. t = mem : point t to the mem
3. construct(mem) : construct the class Test
因为,对于单个线程,两个块是完全等价的。也就是说,您可以放心,这种单例初始化对于单线程应用程序是完全安全的。但是,对于多个线程,一个线程可能会获得部分初始化对象的引用!
当您使用多个线程时,要确保语句之间的发生之前的关系,您有两种可能性:获取/释放锁和读取/写入易失性字段。要修复 DCL,您必须声明包含单例的字段volatile
。这将确保单例的初始化(即,运行其构造函数)发生在任何读取包含单例的字段之前。有关 volatile 如何修复 DCL 的详细说明,请查看我在此问题顶部链接的答案。