Java 中的双重检查锁(Double-Checked Locking)的问题

Java 中的双重检查锁

不知道从什么时候开始,双重锁检机制开始在 Java 程序员中流传开来,并被一些不成熟的程序员所称道。然而,双重锁检机制无论在 Java 的什么年代,都是一个不折不扣的代码陷阱。

1
2
3
4
5
6
7
8
9
10
11
12
13
// double-checked-locking - don't do this!

private static Something instance = null;

public Something getInstance() {
if (instance == null) {
synchronized (this) {
if (instance == null)
instance = new Something();
}
}
return instance;
}

这段代码粗看上去使用了很“神奇”的 Java 多线程技巧,“巧妙”利用重复地两次检查对象是否为空来避免了大部分情况下 synchronized 关键字的性能消耗。

双重锁检的一种优化方案

有很多人对上述代码提出了异议,他们认为,对象的初始化和对象的赋值操作之间可能存在重排序,有一定的可能性 getInstance 方法返回的对象不是完全初始化完成的,因此解决方案是需要为 instance 变量添加 volatile 修饰。看上去这就是完美的解决方案了,volatile 关键字在对象的初始化和对象的赋值操作之间添加了 Happen-before 关系,因此也就不存在重排序的问题了。

然而,首先,volatile 关键字在 Java 1.5 以前不能保证线程获取到的对象是最新的。其次,对象的初始化和对象的赋值操作之间的 Happen-before 关系是在 JMM 被重新修订之后(JSR-133)才出现的。也就是说,在 Java 1.5 以前,添加 volatile 与否对这段代码来说都是一样的效果,volatile 并不能解决任何问题。在 Java 1.5 以后,添加 volatile 与否对这段代码来说也都是一样的效果,因为新的 JMM 已经可以保证对象的初始化和对象的赋值操作之间存在 Happen-before 关系。

扩展阅读:Is volatile expensive?

synchronized 性能很差?

最重要的问题是,为什么用这种方式去写代码?大部分人都会说,因为 synchronized 关键字的性能很差,所以需要用一些技巧来避免大量使用它。那么 synchronized 关键字的性能真的很差吗?

早在 2001 年就有 IBM 的研究员做了相关的调查。

由于同步机制涉及缓存的刷新和失效,Java 中的同步块通常比其他关键机制更为消耗性能,其他机制通常只用“检查并赋值”的机器级别的原子指令来执行。并且,就算只在单核上运行单线程的代码,同步块仍然比非同步的代码要慢。而如果同步需要对锁进行竞争,那么性能就会大大下降。

幸运的是,JVM 的持续改进既改进了整个 Java 程序的性能,也降低了每个版本使用同步的相对成本。此外,同步的性能成本往往被夸大了。众所周知,一个同步的方法调用比不同步的方法调用慢50倍。虽然该声明可能是正确的,但它也具有误导性,并导致许多开发人员在需要的情况下仍想要避免同步。

这篇文章里你能找到对于 synchronized 关键字的一份简单的性能报告,这也许能帮助你理解,synchronized 关键字的性能在大部分情况下并不如你想象中的消耗那么大。并且,不同版本间的比较可以看出,JDK 的持续优化对于同步块的性能提升有多大。

不要再使用双重锁检了

既然双重锁检是错误的写法,并且 synchronized 关键字的性能消耗也不大,那么如果能避免 synchronized 关键字的话,最优的写法是什么呢?

仍然以单例为例:

1
2
3
4
5
6
7
private static class LazySomethingHolder {
public static Something something = new Something();
}

public static Something getInstance() {
return LazySomethingHolder.something;
}

这样编写单例的好处有:

  1. 避免了 synchronized 关键字的性能消耗
  2. 在加载 LazySomethingHolder 类的时候才会初始化单例,懒加载
  3. 代码简洁明了

如果深究的话你会注意到,能够保证对象初始化和赋值之间存在 Happen-before 关系是由于 static 关键字的原因。在最新的 JLS 中有具体说明 JVM 是如何处理这里的逻辑的:JLS 12.4.2

参考资料

  1. JSR 133 FAQ
  2. Fixing the Java Memory Model, Part 2
  3. what is the correct way to guarantee a non-final reference field will never be read as null?