为什么java双重检查锁单例必须使用volatile关键字?
我见过一些类似的问题,但仍有一些困惑。为什么java双重检查锁单例必须使用volatile关键字?,java,concurrency,volatile,Java,Concurrency,Volatile,我见过一些类似的问题,但仍有一些困惑。 代码如下: private volatile static DoubleCheckSingleton instance; private DoubleCheckSingleton() {} public static DoubleCheckSingleton getInstance(){ if(instance==null){ //first synchronized (DoubleCheckSingleton.class){
代码如下:
private volatile static DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance(){
if(instance==null){ //first
synchronized (DoubleCheckSingleton.class){
if(instance==null){ // second
instance=new DoubleCheckSingleton();
}
}
}
return instance;
}
在这个问题中,它表示如果没有volatile
关键字,线程可能会在构造函数完成之前分配实例变量,因此另一个线程可能会看到半构造的对象,这可能会导致严重的问题
但我不明白volatile如何解决这个问题Volatile
用于确保可见性,因此当一个线程将一个半构造的对象分配给实例变量时,另一个线程可以立即看到更改,这会使情况变得更糟
volatile
如何解决这个问题,请有人给我解释一下。谢谢
线程可以在构造函数完成之前分配实例变量
事实并非如此。分配就在代码示例中:
instance=new DoubleCheckSingleton()
显然,在构造函数调用返回之前,执行该赋值的线程不可能进行赋值
问题是,当两个不同的线程在两个不同的处理器上运行而没有任何同步时,它们不一定会同意分配发生的顺序。因此,即使线程A在分配实例
之前分配了新对象的字段(在新DoubleCheckSingleton()
调用中),线程B也可能会发现这些分配顺序不正确。线程B在看到新的DobuleCheckSingleton()所做的其他事情之前,可以先看到对实例的赋值
将instance
声明为volatile
将同步线程volatile
保证当线程B获取volatile
变量的值时,线程A在分配volatile
变量之前所做的一切都将对线程B可见。换句话说,假设我们有两个完全匹配的线程:
- 线程1:“如果实例!=null”。。。好的,它是空的
- 线程2:“如果实例!=null”。。。好的,它是空的
- 线程1:创建一个新的“实例”
- 线程2:踩踏线程1刚刚创建的值
。。。而且,很有可能,两条线程目前都没有意识到出了什么问题。很可能他们都认为自己有一个DoubleCheckSingleton
,并且正在按他们应该的方式使用它们。。。他们都没有意识到有两个。(因此,坐下来,看着数据结构在你的应用程序nose潜入地下时被左右乱扔。)
更糟糕的是,这正是那种“千载难逢”的错误。也就是说,它从未在任何开发人员机器上发生过,但它发生在部署到生产环境的五分钟后<强>;-)强>
volatile关键字基本上是说“这个存储位置本身就是一个共享资源”,事实就是这样
另外:我认为,最佳实践是在启动线程或子进程之前初始化所有同步对象。所有这些实体现在都可以引用这些已经存在的对象并可以使用它们,但这些玩家中没有一个曾经创建过它们。根据Java Concurrency In Practice等人的说法,双重检查锁定作为一种反模式花费了大量时间。当时他们建议使用类似于Lazy Holder模式的东西来代替
Brian Goetz写了一篇关于DCL如何在这里被破坏的好文章
正如StevenC向我指出的,由于Java内存模型的更新,它将正常工作,尽管如此,我认为Lazy Holder仍然是一个不错的选择。它实现起来很干净,并且避免了易失性的需要。DCL的不当实现将导致难以发现的bug “对volatile
字段(§8.3.1.4)的写入发生在该字段的每次后续读取之前”–。如果不使用volatile,您的//第一次测试可能看不到其他线程对变量所做的更改。请参见示例。对于单例,此模式不必要地复杂。看看你在哪里看到“if instance!=null”???另外,在您的描述中,同步锁在两个“if instance==null”之间的作用是什么?请阅读:。请注意,您所建议的(非惰性初始化)在许多用例中都不是可行的选项。所以称之为“最佳实践”显然是错误的。。。。即使你恰当地使用了这个术语。(请随时与您的同事分享该链接。将信息传达出去!)我认为这不是问题所在。第二个if(instance==null)
用于防止您所说的情况发生Goetz文章过时(2001)。在Java5中,修改了内存模型。如果您1)使用Java 5+,2)将实例
声明为volatile
,DCL将不再被破坏。这很公平。。。懒惰的持有者仍然工作得很好。。。并避免诸如“为什么这需要是不稳定的”。。。或者更糟糕的是,没有volatile的不正确实现会导致难以定位错误…编译器是否可能以某种方式重新排序程序,因此它首先分配内存空间并将其分配给实例变量,然后调用构造函数?@haoyuwang,如果编译器决定在线调用构造函数,那么是,我想那是可能发生的。但是可以这样想:javac
编译器可以做什么,JIT编译器(如果有的话)可以做什么,硬件可以做什么。但无论这些事情对您的程序有何影响,最终结果都必须按照(JLS)承诺的方式运行。