使用双重检查锁定实现单例时,我们是否需要 volatile

aar*_*chu 3 java singleton volatile synchronized

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

    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;
    }

Run Code Online (Sandbox Code Playgroud)

我们是否需要将变量“instance”设置为“volatile”?我听到有人说我们需要它来禁用重新排序:

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

address=alloc
instance=someAddress
init(someAddress)
Run Code Online (Sandbox Code Playgroud)

他们说如果最后两个步骤被重新排序,我们需要一个 volatile 实例来禁用重新排序,否则其他线程可能会得到一个没有完全初始化的对象。

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

Eug*_*ene 10

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

 int x = a;
 int y = a;
Run Code Online (Sandbox Code Playgroud)

编译器将这些重新排序为:

 // reverse the order
 int y = a;
 int x = a;
Run Code Online (Sandbox Code Playgroud)

没有人writesa这里,只有两个readsa,因此这种类型的重新排序是允许的。

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

// 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;
}
Run Code Online (Sandbox Code Playgroud)

编译器可能会查看这段代码并注意到如果输入了if(x == 4) { ... }this , this :int z = a;永远不会发生。但是,同时,您可以稍微考虑一下:如果if statement输入了,我们不关心是否int z = a;执行,它不会改变以下事实:

 int y = a;
 return y;
Run Code Online (Sandbox Code Playgroud)

仍然会发生。因此,让我们让它int z = a;变得渴望:

public int test() {

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

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

   return z;
}
Run Code Online (Sandbox Code Playgroud)

现在编译器可以进一步重新排序:

// < --- these two have switched places
int z = a;
int x = a;

if(x == 4) { ... }    
Run Code Online (Sandbox Code Playgroud)

有了这些知识,我们现在可以尝试了解正在发生的事情。

让我们看看你的例子:

 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)
}
Run Code Online (Sandbox Code Playgroud)

有 3 个读取instance(也称为load)和一个读取write(也称为store)。听起来可能很奇怪,但是如果read (1)看到 an instancethat is not null(意味着if (instance == null) { ... }没有输入),这并不意味着read (3)将返回一个非空实例,它read (3)仍然是完全有效的return null。这应该会融化你的大脑(它做过几次)。幸运的是,有一种方法可以证明这一点。

编译器可能会向您的代码添加这么小的优化:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (lock) {
            if (instance == null) {
                instance = new Singleton();
                // < --- we added this
                return instance;
            }
        }
    }
    return instance;
}
Run Code Online (Sandbox Code Playgroud)

它插入了一个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;
}
Run Code Online (Sandbox Code Playgroud)

现在编译器可能会看到这一点,并看到:如果if (local2 == null) { ... }输入,则Singleton local4 = instance;永远不会发生(或者如我在开始这个答案的示例中所说:是否Singleton local4 = instance;发生根本无关紧要)。但是为了进入if (local2 == null) {...},我们需要先进入这个if (local1 == null) { ... }。现在让我们从整体上来推理一下:

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)
Run Code Online (Sandbox Code Playgroud)

您可以看到,在所有情况下,这样做都没有坏处: Singleton local4 = 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;
}
Run Code Online (Sandbox Code Playgroud)

这里有两个独立的读物instance

Singleton local4 = instance; // < --- read (3)
Singleton local1 = instance;   // < --- read (1)

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

return local4;
Run Code Online (Sandbox Code Playgroud)

您读instancelocal4(假设为 a null),然后您读instancelocal1(假设某个线程已将其更改为非空值)并且……您getInstance将返回 a null,而不是 a Singleton。qed


结论:这些优化只有private static Singleton instance;is时才有可能non-volatile,否则大部分优化都会被禁止,这样的事情甚至都不可能。所以,是的,使用volatile是此模式正常工作的必要条件。

  • @Vaibhav 如所说“*类初始化免费提供所有线程安全的延迟初始化*”。因此,只需将该字段声明为“private static final Singleton instance = new Singleton();”,它将在该类第一次使用时初始化,即第一次调用“getInstance()”时。该方法本身就像“public static Singleton getInstance() { return instance; ”一样简单。}` 那么。它是惰性的、线程安全的,甚至比任何 DCL 方法都更高效,因为在类初始化之后,JVM 可以自由地读取字段而无需任何锁定。 (2认同)