为什么要在 Double-Checked-Locking 中添加第二个测试?

Why adding the second test in the Double-Checked-Locking?

参考这个https://en.wikipedia.org/wiki/Double-checked_locking, 我们有:

// "Double-Checked Locking" idiom
class Foo {
    private Helper helper;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }    
    // other functions and members...
}

第二次测试的目的是什么?两个线程真的有可能同时访问同一个临界区吗?

两个线程不能同时访问同一个临界区;根据定义,临界区是互斥的。

为了回答你的问题,第一个 null 测试是没有同步的廉价测试,第二个 null 测试检查同步的真实状态。


第二个测试是必要的。假设我们没有它,代码如下所示:

public Helper getHelper() {
    if (helper == null) {
        synchronized(this) {
            helper = new Helper();
        }
    }
    return helper;
}

假设线程 A 执行 if (helper == null) 并测试 true,因此它进入 if 块但执行被暂停。尚未更新任何变量。

然后线程 B 执行 if (helper == null) 并测试 true,因此它进入 if 块。然后它继续执行到 synchronized 块并初始化 helper 对象和 returns.

现在线程 A 继续执行,进入 synchronized 块,用新对象覆盖 helper,并 returns 对象。

我们遇到的问题是 helper 使用不同的对象初始化了两次。

这就是为什么需要进行第二次测试的原因。

为了更清楚地说明可能发生的情况,请考虑以下代码:

if (helper == null) {
    Thread.sleep(1000);
    synchronized(this) {
        if (helper == null) {
            helper = new Helper();
        }
    }
}

这只是一个例子,所以我不关心 InterruptedException

应该清楚的是,在通过第一个测试和进入临界区之间,其他线程有时间先进入临界区。

除了所有其他答案之外,一个很好的例子是线程安全 Singleton 模式。你又是一样的:

public static Singleton getInstanceDC() {
   if (_instance == null) {       // Single Checked
    synchronized (Singleton.class) { 
         if (_instance == null) { // Double checked 
               _instance = new Singleton();
          }
     }
  }
   return _instance;
}

因此,与指针检查 instance != null 相比,基本上执行锁定的成本要高得多。该实现还必须确保在初始化 Singleton 时不会出现线程竞争条件导致的问题。所以主要原因是性能。如果 instance != null(除第一次外总是如此),则无需进行昂贵的锁定:同时访问已初始化单例的两个线程将不必要地同步。这张图说明的很清楚:

有很多more in Singletons然后再检查一下:

  • 单例模式中的早期和惰性实例化

  • 单例和序列化