java - 使用双重检查锁定实现单例时我们是否需要 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 == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }


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

创建对象时,可能会发生重新排序:
address=alloc
instance=someAddress
init(someAddress)

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

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

最佳答案

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

 int x = a;
 int y = a;

编译器将它们重新排序为:
 // reverse the order
 int y = a;
 int x = a;

没有人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;
}

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

仍然会发生。因此,让我们这样做 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;
}

现在编译器可以进一步重新排序:
// < --- these two have switched places
int z = a;
int x = a;

if(x == 4) { ... }    

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

让我们看看你的例子:
 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)
}
instance有3次读取(也称为 load )和单个 write到它(也称为 store )。听起来很奇怪,但如果 read (1)看过instance不为空(表示未输入if (instance == null) { ... }),并不表示read (3)将返回一个非空实例,它对 read (3) 完全有效仍然返回null .这应该会融化你的大脑(它做了几次我的)。幸运的是,有一种方法可以证明这一点。

编译器可能会为您的代码添加如此小的优化:
public static Singleton getInstance() {
    if (instance == null) {
        synchronized (lock) {
            if (instance == null) {
                instance = new Singleton();
                // < --- we added this
                return instance;
            }
        }
    }
    return instance;
}

它插入了 return instance ,从语义上讲,这不会以任何方式改变代码的逻辑。

然后,有一个certain optimization编译器这样做会帮助我们。我不打算深入研究细节,但它引入了一些本地字段(好处在于该链接)来执行所有读取和写入(存储和加载)。
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 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)

您可以看到,在所有情况下,这样做都没有坏处: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;
}
instance有两个独立的读取这里:
Singleton local4 = instance; // < --- read (3)
Singleton local1 = instance;   // < --- read (1)

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

return local4;

您阅读了instance进入 local4 (假设是 null ),然后您阅读 instance进入 local1 (假设某个线程已经将其更改为非空值)并且...您的 getInstance将返回 null ,而不是 Singleton . q.e.d.

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

关于java - 使用双重检查锁定实现单例时我们是否需要 volatile,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59208041/

相关文章:

java - 无法向 XMLUnit 注册命名空间

java - JHipster - 生成假数据

java - XML 格式的 Web View 位于主栏下方

java - 在 Java 中使用原始类型的安全隐患是什么?

c# - 需要有关 OOP 哲学的建议

java - java中同步方法的问题

javascript - 传递对象的成员函数时绑定(bind) "this"

java - 从域对象中访问 Spring 单例的好方法?

java - java中同步块(synchronized block)之后的代码

java - HashMap同步