【缺陷周话 】第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 问题根源分析

  1. 对象创建过程stringBad = new String()实际上分为三个步骤:

    • 分配对象内存空间
    • 初始化对象
    • 将引用指向分配的内存地址
  2. 指令重排序:JVM可能优化执行顺序为:

    • 分配对象内存空间
    • 将引用指向分配的内存地址
    • 初始化对象
  3. 多线程场景

    • 线程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. 避免双重检查锁定的最佳实践

  1. 优先使用volatile方案(JDK5+)

    • 简单直接
    • 性能影响小
  2. 考虑类初始化方案

    • 更优雅
    • 线程安全由JVM保证
  3. 避免在JDK5之前使用双重检查锁定

    • 无法保证安全性
    • 考虑其他单例实现方式
  4. 代码审查要点

    • 检查单例模式实现
    • 确认是否有多线程访问可能
    • 验证volatile使用正确性
  5. 测试建议

    • 高并发压力测试
    • 长时间运行稳定性测试
    • 边界条件测试

6. 相关CWE条目

CWE ID 609: Double-Checked Locking
描述:软件使用双重检查锁定来延迟初始化,但没有正确实现同步,可能导致在多线程环境中访问未完全初始化的对象。

7. 检测工具示例

使用360代码卫士可以检测此类缺陷:

  • 检测等级:中
  • 检测位置:第一次检查是否为null的代码行
  • 修复验证:修复后检测应不再报告该缺陷

8. 总结

双重检查锁定是一种常见的优化模式,但实现不当会导致严重的多线程问题。在Java中,正确的实现方式包括:

  1. 使用volatile关键字(JDK5+)
  2. 采用基于类初始化的方案

开发者应当理解这些模式的原理和适用场景,避免在多线程环境中引入难以发现的缺陷。代码审查和静态分析工具的使用可以帮助早期发现这类问题。

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