读取(关键资源块外部)和写入(关键资源块内部)如何没有原子性问题。
我已经与不同的人阅读和讨论过,但是大多数人都不回答这两个操作是否都是原子的,以及上述问题实际上是如何实现的。
class ABC {
private static volatile ABC abcInstance;
static ABC getInstance(){
if(abcInstance == null){
synchronized(ABC.class){
if(abcInstance == null){
abcInstance = new ABC();
return abcInstance;
}
}
}
return abcInstance;
}
}
是
if(abcInstance == null) outside synchronisation block
和abcInstance = new ABC();
原子的,如果不是,则这种创建单例的方法是错误的。在C ++中,
abcInstance = new ABC();
大致由三个指令组成:创建ABC对象。
为ABC分配内存。
将其分配给abcInstance。
为了优化,编译器可以以任何方式对这三个指令进行重新排序。假设它遵循2-> 3-> 1,并且在指令3发生中断之后,调用getInstance()的下一个线程将读取abcInstance具有某个值,然后它将指向不具有ABC对象的对象。
如果C ++和Java都错了,请纠正我。
最佳答案
这仅回答您问题的Java部分。
是if(abcInstance == null)
和abcInstance = new ABC();
是原子的,如果不是,则这种创建单例的方法是错误的。
(潜在的)问题不是原子性。 (从执行分配的线程和读取分配的变量的线程的角度来看,引用分配是原子的。)
问题是当写入abcInstance
的值对另一个线程可见时。
在Java 5之前,内存模型没有为该实现可靠地提供足够的内存可见性保证。
在Java 5(和更高版本)的内存模型中,一个线程对volatile变量的写入与另一个线程对变量的后续读取之间的关系发生了。这表示:
如果第一个线程已写入第二个线程,则保证第二个线程看到abcInstance
的非空值。
发生在关系之前还可以确保第二个线程将看到第一个线程创建的ABC
实例的完全初始化状态。synchronized
块可确保一次只能创建一个ABC
实例。
这是权威文章,解释了为什么旧的双重检查锁定实现被破坏:
The "Double-Checked Locking is Broken" Declaration
如安德鲁·特纳(Andrew Turner)所述,在Java中有一种更简单,更干净的方法来实现单例类:使用enum
。
Implementing Singleton with an Enum (in Java)