假设我们使用双重检查锁定来实例化一个单例:
public static Instance getInstance() {
if (this.instance == null) {
synchronized(Instance.class) {
if (this.instance == null) {
this.instance = new Instance();
}
}
}
return this.instance;
}
如果 instance
变量将是 volatile 并且将删除双重检查锁定,则问题在于程序的语义。
private volatile static Instance instance;
public static Instance getInstance() {
if (this.instance == null) {
this.instance = new Instance();
}
return this.instance;
}
这个类只会被实例化一次吗?或者,换句话说, volatile 读取是否会以这样一种方式发生冲突,即两个线程将看到引用的 null
值并执行双重实例化?
我知道 volatile 写入和 volatile 读取之间的happens-before关系,并且 volatile 禁止缓存(因此所有读取和写入都将在主内存中执行,而不是在处理器的缓存中执行),但在并发 volatile 读取的情况下尚不清楚。
P.S.:问题不在于单例模式的应用(这只是一个问题很明显的例子),它只是关于是否可以用 volatile 读取替换双重检查锁定 - volatile 写入而不改变程序语义,仅此而已。
最佳答案
如果没有同步,您的代码肯定会出错,因为 2 个线程可能会看到 instance 的值为 null,并且都将执行初始化(考虑在每一行进行上下文切换,看看会发生什么。
除此之外,即使使用同步双重检查锁定 (DCL),过去在 Java 中也被认为是错误的,因为在非同步运行时,第二个线程可能会以不同的顺序经历初始化操作。 您可以通过添加一个局部变量来修复您的代码,并在您想要读取它时将 volatile 加载到其中:
public static Instance getInstance() {
Instance tmp = instance;
if (tmp == null) {
synchronized(Instance.class) {
Instance tmp = instance;
if (tmp == null) {
instance = new Instance();
}
}
}
return instance;
}
但更安全的解决方案是使用 ClassLoader 作为您的同步机制,并且还允许您在每次访问单例时停止使用慢速 volatile 访问:
public class Instance {
private static class Lazy {
private static Instance INSTANCE = new Instance();
}
public static Instance getInstance() {
return Lazy.INSTANCE;
}
}
INSTANCE
只有在第一个线程进入getInstance()
时才会被初始化
关于java - volatile 读取冲突,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52930084/