在我开始解释之前,您需要了解编译器执行的一种优化(我的解释非常简化)。假设在您的代码中有如下序列:
int x = a;
int y = a;
编译器重新排序这些顺序是完全有效的:
// reverse the order
int y = a;
int x = a;
这里没有人对a
进行写入
,只有两个读取
a
的操作,因此允许这种类型的重新排序。
稍微更复杂的例子是:
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语句
,我们并不在乎是否执行int z = a;
,因为它不会改变这个事实:
int y = a;
return y;
仍会发生这种情况。 因此,让我们将int z = a;
变为急切:
public int test() {
int x = a;
int z = a;
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;
public static Singleton getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
instance
被读取3次(也叫做
load
),只被写入1次(也叫做
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();
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
读取:
Singleton local4 = instance; // <
Singleton local1 = instance; // <
if(local1 == null) {
....
}
return local4;
你将instance
读入local4
(假设它是null
),然后你将instance
读入local1
(假设某个线程已将其更改为非空),如果此时有一个线程正在执行同步块并创建单例实例,那么您的getInstance
将返回null
而不是Singleton
。q.e.d.
结论:这些优化仅在private static Singleton instance;
是non-volatile
时才可能实现,否则大部分优化被禁止,甚至不可能出现这样情况。因此,使用volatile
对于此模式的正确运行是必须的。
如果x和y是同一线程的操作,并且x在程序顺序中出现在y之前,则hb(x,y)。*另请参见https://stackoverflow.com/questions/42079959/explain-how-jit-reordering-works
- Scary Wombat