双重检查锁定不是线程安全的¶
ID: java/unsafe-double-checked-locking
Kind: problem
Security severity:
Severity: error
Precision: high
Tags:
- reliability
- correctness
- concurrency
- external/cwe/cwe-609
Query suites:
- java-security-and-quality.qls
双重检查锁定是一种常见模式,用于对多个线程访问的字段进行延迟初始化。然而,根据底层运行时的内存模型,它可能很难正确实现,因为编译器、运行时或 CPU 执行的重新排序可能会将未初始化或半初始化的对象暴露给其他线程。自版本 5 起,Java 改进了其内存模型,以支持双重检查锁定,前提是底层字段标记为 volatile
,并且所有初始化都发生在 volatile 写入之前。
建议¶
首先,应该考虑执行延迟初始化的 getter 是否对性能至关重要。如果不是,一个更简单的解决方案是完全避免双重检查锁定,只需将整个 getter 标记为 synchronized
。这更容易实现,并且可以防止难以发现的并发错误。
如果使用双重检查锁定,则底层字段必须为 volatile
,并且对字段的更新必须是同步区域中发生的最晚一件事,即所有初始化都必须在分配字段之前完成。此外,Java 版本必须为 5 或更高版本。读取 volatile
字段会产生轻微的开销,因此使用局部变量来最大程度减少 volatile 读取次数也很有用。
示例¶
以下代码延迟初始化 f
为 new MyObject()
。
private Object lock = new Object();
private MyObject f = null;
public MyObject getMyObject() {
if (f == null) {
synchronized(lock) {
if (f == null) {
f = new MyObject(); // BAD
}
}
}
return f;
}
此代码不是线程安全的,因为另一个线程可能会在构造函数完成求值之前看到对 f
的赋值,例如,如果编译器内联内存分配和构造函数,并将对 f
的赋值重新排序为在内存分配之后立即发生。
另一个示例,即使使用了 volatile
,也不是线程安全的,如果在对 f
进行赋值后发生了其他初始化,因为其他线程可能会在对象完全初始化之前访问该对象,即使编译器或运行时没有进行任何重新排序。
private Object lock = new Object();
private volatile MyObject f = null;
public MyObject getMyObject() {
if (f == null) {
synchronized(lock) {
if (f == null) {
f = new MyObject();
f.init(); // BAD
}
}
}
return f;
}
应重写上述代码以同时使用 volatile
,并在更新 f
之前完成所有初始化。此外,可以使用局部变量来避免多次读取字段。
private Object lock = new Object();
private volatile MyObject f = null;
public MyObject getMyObject() {
MyObject result = f;
if (result == null) {
synchronized(lock) {
result = f;
if (result == null) {
result = new MyObject();
result.init();
f = result; // GOOD
}
}
}
return result;
}
最后需要注意的是,如果构造的对象不可变(即对象将所有字段声明为 final
),并且在同步块外部只读取一次双重检查字段,则可以在没有 volatile
的情况下正确使用双重检查锁定。
鉴于 MyImmutableObject
中的所有字段都声明为 final
,因此以下示例可防止将未初始化的字段暴露给其他线程。但是,由于在没有同步的情况下读取 f
两次,因此有可能对它们进行重新排序,这意味着此方法可以返回 null
。
private Object lock = new Object();
private MyImmutableObject f = null;
public MyImmutableObject getMyImmutableObject() {
if (f == null) {
synchronized(lock) {
if (f == null) {
f = new MyImmutableObject();
}
}
}
return f; // BAD
}
在这种情况下,使用局部变量来最小化字段读取次数不再是性能提升,而是一个对正确性至关重要的细节。
参考资料¶
Java 语言规范:17.4. 内存模型。
维基百科:双重检查锁定。
Aleksey Shipilëv:Java 中的安全发布和安全初始化。
Aleksey Shipilëv:Java 内存模型类型的近距离接触。
常见弱点枚举:CWE-609。