假设我们使用双重检查锁来实现单例模式:
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;
}
我们需要将变量“实例”设置为“易失性”吗?我听到一句话说我们需要它来禁用重新排序:
创建对象时,可能会发生重新排序:
address=alloc
instance=someAddress
init(someAddress)
他们说,如果对最后两个步骤进行了重新排序,我们需要一个易失性实例来禁用重新排序,否则其他线程可能会得到一个未完全初始化的对象。
但是由于我们处于同步代码块中,我们是否真的需要volatile?还是一般来说,我是否可以说同步块可以保证共享变量对其他线程是透明的,即使它不是volatile变量也不会重新排序?
在进行这种解释之前,您需要了解编译器所做的一种优化(我的解释非常简化)。假设在代码的某个地方有这样一个序列:
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)
}
有3个instance
(也称为load
)读取,并且有一个write
(也称为store
)。听起来很奇怪,但是如果read (1)
看到不为null的instance
(表示未输入if (instance == null) { ... }
),并不意味着read (3)
将返回非null实例,对于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 (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
在进行检查之前,如果有。
疯狂之后,您的代码可能会变成:
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
时,这些优化是only的,否则很多优化将被禁止,甚至不可能实现。因此,是的,必须使用volatile
才能使此模式正常工作。