Java 在使用双重检查锁定实现单例时是否需要volatile

Java 在使用双重检查锁定实现单例时是否需要volatile,java,singleton,synchronized,volatile,Java,Singleton,Synchronized,Volatile,假设我们使用双重检查锁来实现单例模式: private static Singleton instance; private static Object lock = new Object(); public static Singleton getInstance() { if(instance == null) { synchronized (lock) { if(instance == nu

假设我们使用双重检查锁来实现单例模式:

    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”?我听到一句话,我们需要它来禁用重新排序:

创建对象时,可能会发生重新排序:

address=alloc
instance=someAddress
init(someAddress)
他们说,如果最后两个步骤被重新排序,我们需要一个volatile实例来禁用重新排序,否则其他线程可能会得到一个未完全初始化的对象


然而,由于我们处于同步代码块中,我们真的需要volatile吗?或者,一般来说,我可以说synchronized block可以保证共享变量对其他线程是透明的,并且即使它不是易失变量也不会重新排序吗?

在我开始解释之前,您需要了解编译器所做的一个优化(我的解释非常简单)。假设您的代码中有这样一个序列:

 int x = a;
 int y = a;
编译器完全可以将它们重新排序为:

 // reverse the order
 int y = a;
 int x = a;
没有人
写入
a
此处,只有两次
读取
a
,因此允许这种类型的重新排序

一个稍微复杂一点的例子是:

// 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){…}
,那么这段代码:
intz=A从不发生。但是,同时,您可以考虑稍微不同的情况:如果输入了
if语句,我们不关心
intz=a,它都不会改变以下事实:

 int y = a;
 return y;
仍然会发生。因此,让我们把
intz=a渴望:

public int test() {

   int x = a;
   int z = a; // < --- this jumped in here

   if(x == 4) {
       int y = a;
       return y;
   }

   return z;
}
它插入了一个
返回实例
,从语义上讲,这不会以任何方式更改代码的逻辑

然后,编译器所做的一些工作将对我们有所帮助。我不打算深入讨论细节,但它引入了一些本地字段(好处在于该链接)来执行所有读写操作(存储和加载)

您可以看到,在所有情况下,这样做都没有害处:
singletonlocal4=instance
在任何if检查之前

在经历了所有这些疯狂之后,您的代码可能会变成:

 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
读入
local4
(假设为
null
),然后将
instance
读入
local1
(假设某个线程已将其更改为非null)并。。。您的
getInstance
将返回一个
null
,而不是一个
Singleton
。q、 急诊室



结论:只有当
私有静态单例实例时,这些优化才可能实现
是非易失性的
,否则很多优化都是被禁止的,这样的优化是不可能的。因此,是的,使用
volatile
是此模式正常工作的必要条件

这些信息的来源在哪里?只是同事们的讨论。真正的问题是,为什么我们要对单例使用双重检查锁定(而不是
enum
或其他实现方法)?真正的问题是:我们需要将这个共享实例变量设置为“volitile”吗?这似乎也是多线程的问题。如果我们有两个动作x和y,我们写hb(x,y)来表示x发生在y之前。如果x和y是同一线程的动作,并且x在程序顺序中位于y之前,那么hb(x,y)。还可以看到更可怕的是,如果你看到一个未完全构造的对象。毕竟,如果不是易失性的,编译器可能会将构造函数中的存储重新排序为
实例
。@JohannesKuhn是的,你说的是安全发布,为了正确使用
易失性
,读写器必须在同一
易失性
上成对工作。
 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)
}
public static Singleton getInstance() {
    if (instance == null) {
        synchronized (lock) {
            if (instance == null) {
                instance = new Singleton();
                // < --- we added this
                return instance;
            }
        }
    }
    return instance;
}
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 (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)
 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;
}
Singleton local4 = instance; // < --- read (3)
Singleton local1 = instance;   // < --- read (1)

if(local1 == null) {
   ....
}

return local4;