为什么要在 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然后再检查一下:
单例模式中的早期和惰性实例化
单例和序列化
参考这个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然后再检查一下:
单例模式中的早期和惰性实例化
单例和序列化