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

问题描述 投票:2回答:1

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

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

我们需要将变量“实例”设置为“易失性”吗?我听到一句话说我们需要它来禁用重新排序:

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

address=alloc
instance=someAddress
init(someAddress)

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

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

java singleton synchronized volatile
1个回答
4
投票

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

 int x = a;
 int y = a;

对于编译器而言,将它们重新排序为:是完全有效的。

 // reverse the order
 int y = a;
 int x = a;

这里没有writesareads中只有两个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) { ... },则此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)
}

有3个instance(也称为load)读取,并且有一个write(也称为store)。听起来很奇怪,但是如果read (1)看到不为null的instance(表示未输入if (instance == null) { ... }),并不意味着read (3)将返回非null实例,对于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 (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 在进行检查之前,如果有

疯狂之后,您的代码可能会变成:

 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时,这些优化是only的,否则很多优化将被禁止,甚至不可能实现。因此,是的,必须使用volatile才能使此模式正常工作。

© www.soinside.com 2019 - 2024. All rights reserved.