问题描述
假设我们使用双重检查锁来实现单例模式:
suppose we use double-check lock to implement singleton pattern:
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"吗?我听到一句话说,我们需要它来禁用重新排序:
Do we need to set variable "instance" as "volatile"? I hear a saying that we need it to disable reordering:
创建对象时,可能会发生重新排序:
When an object is created , reordering may happen:
address=alloc
instance=someAddress
init(someAddress)
他们说,如果对最后两个步骤进行了重新排序,我们需要一个易失性实例来禁用重新排序,否则其他线程可能会得到一个未完全初始化的对象.
They say that if the last two steps are reordered, we need a volatile instance to disable reordering otherwise other threads may get an object that is not completely initiaized.
但是,由于我们处于同步代码块中,因此我们真的需要volatile吗?还是一般来说,我可以说同步块可以保证共享变量对其他线程是透明的,即使它不是volatile变量也不会重新排序?
However since we are in a synchronized code block, do we really need volatile? Or generally, can I say that synchronized block can guarantee that a shared variable is transparent to other threads and there is no reordering even if it is not volatiled variable?
推荐答案
在进行此解释之前,您需要了解编译器所做的一种优化(我的解释非常简化).假设在代码的某个地方有这样一个序列:
Before I go into this explanation, you need to understand one optimization that compilers do (my explanation is very simplified). Suppose that somewhere in your code you have such a sequence:
int x = a;
int y = a;
对于编译器将它们重新排序为:
It is perfectly valid for a compiler to re-order these into:
// reverse the order
int y = a;
int x = a;
这里没有人将 a
写入 a
,只有两个 a
的 reads
允许重新排序.
No one writes
to a
here, there are only two reads
of a
, as such this type of re-ordering is allowed.
一个稍微复杂的例子是:
A slightly more complicated example would be:
// 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语句
,我们不在乎是否执行了 int z = a;
,它不会改变以下事实:
A compiler might look at this code and notice that if this is entered if(x == 4) { ... }
, this : int z = a;
never happens. But, at the same time, you can think about it slightly different: if that if statement
is entered, we do not care if int z = a;
is executed or not, it does not change the fact that:
int y = a;
return y;
仍然会发生.因此,让我们急于 int z = a;
:
would still happen. As such let's make that int z = a;
to be eager:
public int test() {
int x = a;
int z = a; // < --- this jumped in here
if(x == 4) {
int y = a;
return y;
}
return z;
}
现在,编译器可以进一步重新排序:
And now a compiler can further re-order:
// < --- these two have switched places
int z = a;
int x = a;
if(x == 4) { ... }
有了这些知识,我们现在可以尝试了解正在发生的事情.
Armed with this knowledge, we can try to understand now what is going on.
让我们看看您的示例:
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
.这应该使您的大脑融化(确实发生了几次).幸运的是,有一种方法可以证明这一点.
There are 3 reads of instance
(also called load
) and a single write
to it (also called store
). As weird at it may sound, but if read (1)
has seen an instance
that is not null (meaning that if (instance == null) { ... }
is not entered), it does not mean that read (3)
will return a non-null instance, it is perfectly valid for read (3)
to still return null
. This should melt your brain (it did mine a few times). Fortunately, there is a way to prove this.
编译器可能会在您的代码中添加如此小的优化:
A compiler might add such a small optimization to your code:
public static Singleton getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new Singleton();
// < --- we added this
return instance;
}
}
}
return instance;
}
它插入了一个返回实例
,从语义上讲,这丝毫没有改变代码的逻辑.
It inserted a return instance
, semantically this does not change the logic of the code in any way.
然后,有一个某些优化,编译器可以做到这一点对我们有帮助.我不会详细介绍它,但是它引入了一些本地字段(该链接的好处是)来执行所有读写操作(存储和加载).
Then, there is a certain optimization that compilers do that will help us here. I am not going to dive into the details, but it introduces some local fields (the benefit is in that link) to do all the reads and writes (stores and loads).
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){...}
.现在让我们从整体上对此进行推理:
Now a compiler might look at this and see that: if if (local2 == null) { ... }
is entered, Singleton local4 = instance;
never happens (or as said in the example I started this answer with: it does not really matter if Singleton local4 = instance;
happens at all). But in order to enter if (local2 == null) {...}
, we need to enter this if (local1 == null) { ... }
first. And now let's reason about this as a whole:
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校验之前.
You can see that in all the cases, there is no harm in doing that : Singleton local4 = instance
before any if checks.
疯狂之后,您的代码可能会变成:
After all this madness, your code could become:
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
的两个独立读物:
There are two independent reads of instance
here:
Singleton local4 = instance; // < --- read (3)
Singleton local1 = instance; // < --- read (1)
if(local1 == null) {
....
}
return local4;
您将 instance
读入 local4
(假设为 null
),然后将 instance
读入 local1
(假设某个线程已将其更改为非null),并且...您的 getInstance
将返回 null
,而不是 Singleton
.q.e.d.
You read instance
into local4
(let's suppose a null
), then you read instance
into local1
(let's assume that some thread already changed this into a non-null) and ... your getInstance
will return a null
, not a Singleton
. q.e.d.
结论:仅当私有静态Singleton实例时,这些优化才是 可能;
是非易失性
,否则大多数优化被禁止,并且像这样的事情甚至是不可能的.因此,是的,必须使用 volatile
才能使此模式正常工作.
Conclusion: these optimizations are only possible when private static Singleton instance;
is non-volatile
, otherwise much of the optimization are prohibited and nothing like this would be even possible. So, yes, using volatile
is a MUST for this pattern to work correctly.
这篇关于使用双重检查锁定实现单例时,我们是否需要使用volatile的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!