aar*_*chu 3 java singleton volatile synchronized
假设我们使用双重检查锁来实现单例模式:
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;
}
Run Code Online (Sandbox Code Playgroud)
我们是否需要将变量“instance”设置为“volatile”?我听到有人说我们需要它来禁用重新排序:
创建对象时,可能会发生重新排序:
address=alloc
instance=someAddress
init(someAddress)
Run Code Online (Sandbox Code Playgroud)
他们说如果最后两个步骤被重新排序,我们需要一个 volatile 实例来禁用重新排序,否则其他线程可能会得到一个没有完全初始化的对象。
然而,既然我们在一个同步代码块中,我们真的需要 volatile 吗?或者一般来说,我可以说同步块可以保证共享变量对其他线程是透明的,即使它不是 volatile 变量也不会重新排序吗?
Eug*_*ene 10
在我进入这个解释之前,您需要了解编译器所做的一种优化(我的解释非常简化)。假设在您的代码中某处有这样一个序列:
int x = a;
int y = a;
Run Code Online (Sandbox Code Playgroud)
编译器将这些重新排序为:
// reverse the order
int y = a;
int x = a;
Run Code Online (Sandbox Code Playgroud)
没有人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;
}
Run Code Online (Sandbox Code Playgroud)
编译器可能会查看这段代码并注意到如果输入了if(x == 4) { ... }this , this :int z = a;永远不会发生。但是,同时,您可以稍微考虑一下:如果if statement输入了,我们不关心是否int z = a;执行,它不会改变以下事实:
int y = a;
return y;
Run Code Online (Sandbox Code Playgroud)
仍然会发生。因此,让我们让它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;
}
Run Code Online (Sandbox Code Playgroud)
现在编译器可以进一步重新排序:
// < --- these two have switched places
int z = a;
int x = a;
if(x == 4) { ... }
Run Code Online (Sandbox Code Playgroud)
有了这些知识,我们现在可以尝试了解正在发生的事情。
让我们看看你的例子:
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)
}
Run Code Online (Sandbox Code Playgroud)
有 3 个读取instance(也称为load)和一个读取write(也称为store)。听起来可能很奇怪,但是如果read (1)看到 an instancethat is not null(意味着if (instance == null) { ... }没有输入),这并不意味着read (3)将返回一个非空实例,它read (3)仍然是完全有效的return null。这应该会融化你的大脑(它做过几次)。幸运的是,有一种方法可以证明这一点。
编译器可能会向您的代码添加这么小的优化:
public static Singleton getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new Singleton();
// < --- we added this
return instance;
}
}
}
return instance;
}
Run Code Online (Sandbox Code Playgroud)
它插入了一个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;
}
Run Code Online (Sandbox Code Playgroud)
现在编译器可能会看到这一点,并看到:如果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)
Run Code Online (Sandbox Code Playgroud)
您可以看到,在所有情况下,这样做都没有坏处: 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;
}
Run Code Online (Sandbox Code Playgroud)
这里有两个独立的读物instance:
Singleton local4 = instance; // < --- read (3)
Singleton local1 = instance; // < --- read (1)
if(local1 == null) {
....
}
return local4;
Run Code Online (Sandbox Code Playgroud)
您读instance入local4(假设为 a null),然后您读instance入local1(假设某个线程已将其更改为非空值)并且……您getInstance将返回 a null,而不是 a Singleton。qed
结论:这些优化只有在private static Singleton instance;is时才有可能non-volatile,否则大部分优化都会被禁止,这样的事情甚至都不可能。所以,是的,使用volatile是此模式正常工作的必要条件。