在我进行解释之前,您需要了解编译器所做的一项优化(我的解释非常简单)。假设您的代码中的某处有这样的序列:
int x = a;
int y = a;
编译器将它们重新排序为完全有效:
// reverse the order
int y = a;
int x = a;
No one writes
to a
这里,只有两个reads
of a
,因此允许这种类型的重新排序。
一个稍微复杂一点的例子是:
// someone, somehow sets this
int a;
public int test() {
int x = a;
if(x == 4) {
int y = a;
return y;
}
int z = a;
return z;
}
编译器可能会查看此代码并注意到如果输入了此代码if(x == 4) { ... }
, 这 :int z = a;
永远不会发生。但是,同时,你可以稍微不同地思考一下:如果if statement
输入后,我们不在乎int z = a;
无论执行与否,都不会改变以下事实:
int y = a;
return y;
仍然会发生。因此,让我们这样做int z = a;
渴望:
public int test() {
int x = a;
int z = a; // < --- this jumped in here
if(x == 4) {
int y = a;
return y;
}
return z;
}
现在编译器可以进一步重新排序:
// < --- these two have switched places
int z = a;
int x = a;
if(x == 4) { ... }
有了这些知识,我们现在就可以尝试了解正在发生的事情。
让我们看看你的例子:
private static Singleton instance; // non-volatile
public static Singleton getInstance() {
if (instance == null) { // < --- read (1)
synchronized (lock) {
if (instance == null) { // < --- read (2)
instance = new Singleton(); // < --- write
}
}
}
return instance; // < --- read (3)
}
有 3 次阅读instance
(也叫load
)和一个单一的write
到它(也称为store
)。听起来可能很奇怪,但如果read (1)
见过一个instance
那不为空(意味着if (instance == null) { ... }
未输入),这并不意味着read (3)
将返回一个非空实例,它完全有效read (3)
仍然返回null
。这应该会融化你的大脑(我的也有好几次)。幸运的是,有一种方法可以证明这一点。
编译器可能会向您的代码添加这样一个小的优化:
public static Singleton getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new Singleton();
// < --- we added this
return instance;
}
}
}
return instance;
}
它插入了一个return instance
,从语义上讲,这不会以任何方式改变代码的逻辑。
那么,有一个一定的优化编译器这样做会对我们有所帮助。我不会深入讨论细节,但它引入了一些本地字段(好处在于该链接)来执行所有读取和写入(存储和加载)。
public static Singleton getInstance() {
Singleton local1 = instance; // < --- read (1)
if (local1 == null) {
synchronized (lock) {
Singleton local2 = instance; // < --- read (2)
if (local2 == null) {
Singleton local3 = new Singleton();
instance = local3; // < --- write (1)
return local3;
}
}
}
Singleton local4 = instance; // < --- read (3)
return local4;
}
现在编译器可能会看到这个并看到:如果if (local2 == null) { ... }
已输入,Singleton local4 = instance;
永远不会发生(或者正如我开始这个答案的例子中所说:如果Singleton local4 = instance;
发生)。但为了进入if (local2 == null) {...}
,我们需要输入这个if (local1 == null) { ... }
第一的。现在让我们从整体上推理一下:
if (local1 == null) { ... } NOT ENTERED => NEED to do : Singleton local4 = instance
if (local1 == null) { ... } ENTERED && if (local2 == null) { ... } NOT ENTERED
=> MUST DO : Singleton local4 = instance.
if (local1 == null) { ... } ENTERED && if (local2 == null) { ... } ENTERED
=> CAN DO : Singleton local4 = instance. (remember it does not matter if I do it or not)
您可以看到,在所有情况下,这样做都没有坏处:Singleton local4 = instance
在任何 if 检查之前.
经过所有这些疯狂之后,你的代码可能会变成:
public static Singleton getInstance() {
Singleton local4 = instance; // < --- read (3)
Singleton local1 = instance; // < --- read (1)
if (local1 == null) {
synchronized (lock) {
Singleton local2 = instance; // < --- read (2)
if (local2 == null) {
Singleton local3 = new Singleton();
instance = local3; // < --- write (1)
return local3;
}
}
}
return local4;
}
有两个独立的读取instance
here:
Singleton local4 = instance; // < --- read (3)
Singleton local1 = instance; // < --- read (1)
if(local1 == null) {
....
}
return local4;
你读instance
into local4
(我们假设一个null
),然后你读到instance
into local1
(假设某个线程已经将其更改为非空)并且...您的getInstance
将返回一个null
, not a Singleton
. q.e.d.
结论:这些优化是only可能的时候private static Singleton instance;
is non-volatile
,否则很多优化都会被禁止,这样的事情根本不可能发生。所以,是的,使用volatile
是此模式正常工作的必须条件。