假设我们使用双重检查锁来实现单例模式:
private static Singleton instance;
private static Object lock = new Object();
public static Singleton getInstance() {
if(instance == null) {
synchronized (lock) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
我们是否需要将变量“instance”设置为“volatile”?我听说我们需要它来禁用重新排序:
创建对象时,可能会发生重新排序:
address=alloc
instance=someAddress
init(someAddress)
他们说如果最后两个步骤被重新排序,我们需要一个 volatile 实例来禁用重新排序,否则其他线程可能会得到一个未完全初始化的对象。
但是,由于我们处于同步代码块中,我们真的需要 volatile 吗?或者一般来说,我可以说同步块(synchronized block)可以保证共享变量对其他线程是透明的,即使它不是 volatiled 变量也不会重新排序?
最佳答案
在我进入这个解释之前,你需要了解编译器所做的一个优化(我的解释非常简化)。假设您的代码中某处有这样一个序列:
int x = a;
int y = a;
编译器将它们重新排序为:
// reverse the order
int y = a;
int x = a;
没有人
writes
至a
这里只有两个reads
的 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)
}
instance
有3次读取(也称为 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
,从语义上讲,这不会以任何方式改变代码的逻辑。然后,有一个certain optimization编译器这样做会帮助我们。我不打算深入研究细节,但它引入了一些本地字段(好处在于该链接)来执行所有读取和写入(存储和加载)。
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
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; // < --- read (3)
Singleton local1 = instance; // < --- read (1)
if(local1 == null) {
....
}
return local4;
您阅读了
instance
进入 local4
(假设是 null
),然后您阅读 instance
进入 local1
(假设某个线程已经将其更改为非空值)并且...您的 getInstance
将返回 null
,而不是 Singleton
. q.e.d.结论:这些优化只有在
private static Singleton instance;
时才有可能。是 non-volatile
, 否则很多优化都是被禁止的,这样的事情甚至是不可能的。所以,是的,使用 volatile
是此模式正常工作的必要条件。
关于java - 使用双重检查锁定实现单例时我们是否需要 volatile,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59208041/