【缺陷周话 】第23期:双重检查锁定
字数 1284 2025-08-18 11:37:57
双重检查锁定缺陷分析与修复指南
1. 双重检查锁定概述
双重检查锁定(Double-Checked Locking)是一种用于减少并发系统中竞争和同步开销的软件设计模式。它主要用于延迟高开销的对象初始化操作,在普通单例模式的基础上,先判断对象是否已经被初始化,再决定要不要加锁。
1.1 设计目的
- 延迟高开销的对象初始化操作
- 减少并发系统中的竞争和同步开销
- 避免不必要的同步锁获取
2. 双重检查锁定的危害
双重检查锁定在单线程环境中表现正常,但在多线程环境下存在严重问题:
- 线程切换问题:线程随时会相互切换执行
- 指令重排问题:对象未实例化完全时可能被其他线程访问
- 对象状态不一致:可能导致程序调用未完全初始化的对象
3. 缺陷代码示例分析
以下是一个存在双重检查锁定缺陷的Java代码示例:
public class CWE609_Double_Checked_Locking__Servlet_01 {
private String stringBad;
public String getStringBad() {
if (stringBad == null) { // 第一次检查
synchronized (this) {
if (stringBad == null) { // 第二次检查
stringBad = new String(); // 问题所在
}
}
}
return stringBad;
}
}
3.1 问题根源分析
-
对象创建过程:
stringBad = new String()实际上分为三个步骤:- 分配对象内存空间
- 初始化对象
- 将引用指向分配的内存地址
-
指令重排序:JVM可能优化执行顺序为:
- 分配对象内存空间
- 将引用指向分配的内存地址
- 初始化对象
-
多线程场景:
- 线程1执行到将引用指向内存地址但未完成初始化
- 线程2检查
stringBad != null,直接返回未完全初始化的对象 - 线程2使用该对象时出错
4. 修复方案
4.1 使用volatile关键字
public class FixedDoubleCheckedLocking {
private volatile String stringGood;
public String getStringGood() {
if (stringGood == null) {
synchronized (this) {
if (stringGood == null) {
stringGood = new String();
}
}
}
return stringGood;
}
}
修复原理:
volatile关键字确保:- 可见性:一个线程修改后,其他线程立即可见
- 禁止指令重排序:确保对象完全初始化后才赋值给引用
注意事项:
- 此方案仅适用于JDK5及以上版本
- JDK5之前即使使用volatile也不安全
4.2 基于类初始化的解决方案
public class Singleton {
private static class SingletonHolder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
优点:
- 利用JVM类初始化机制保证线程安全
- 延迟初始化效果
- 无需显式同步
5. 避免双重检查锁定的最佳实践
-
优先使用volatile方案(JDK5+)
- 简单直接
- 性能影响小
-
考虑类初始化方案
- 更优雅
- 线程安全由JVM保证
-
避免在JDK5之前使用双重检查锁定
- 无法保证安全性
- 考虑其他单例实现方式
-
代码审查要点
- 检查单例模式实现
- 确认是否有多线程访问可能
- 验证volatile使用正确性
-
测试建议
- 高并发压力测试
- 长时间运行稳定性测试
- 边界条件测试
6. 相关CWE条目
CWE ID 609: Double-Checked Locking
描述:软件使用双重检查锁定来延迟初始化,但没有正确实现同步,可能导致在多线程环境中访问未完全初始化的对象。
7. 检测工具示例
使用360代码卫士可以检测此类缺陷:
- 检测等级:中
- 检测位置:第一次检查是否为null的代码行
- 修复验证:修复后检测应不再报告该缺陷
8. 总结
双重检查锁定是一种常见的优化模式,但实现不当会导致严重的多线程问题。在Java中,正确的实现方式包括:
- 使用volatile关键字(JDK5+)
- 采用基于类初始化的方案
开发者应当理解这些模式的原理和适用场景,避免在多线程环境中引入难以发现的缺陷。代码审查和静态分析工具的使用可以帮助早期发现这类问题。