为什么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)承诺的方式运行。