掌握 Java 的多线程设计模式:Double-Checked Locking

目录

一、模式定义与核心目标

二、传统单例模式的缺陷

2.1 非线程安全的懒汉式(无锁)

2.2 线程安全的懒汉式(全方法加锁)

三、Double-Checked Locking 的实现原理

3.1 基础实现(JDK 1.5 前有缺陷)

3.2 指令重排序问题与 volatile 修正

四、模式的适用场景

4.1 单例模式的高性能实现

4.2 非单例场景的扩展应用

五、优缺点分析

5.1 优点

5.2 缺点

六、与其他单例模式的对比

七、常见错误与最佳实践

7.1 常见错误

7.2 最佳实践

八、总结


一、模式定义与核心目标

Double-Checked Locking(双重检查锁定) 是一种用于在多线程环境下高效实现单例模式的设计模式。其核心目标是:

  • 线程安全:确保单例实例在多线程环境下唯一创建。
  • 性能优化:避免传统 synchronized 关键字对整个方法的锁竞争,仅在必要时加锁。

二、传统单例模式的缺陷

2.1 非线程安全的懒汉式(无锁)

public class Singleton {
    private static Singleton instance;
    
    private Singleton() {}
    
    // 多线程下可能创建多个实例
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // 非原子操作
        }
        return instance;
    }
}

问题

  • 多线程并发时,可能多个线程同时通过 instance == null 检查,创建多个实例。

2.2 线程安全的懒汉式(全方法加锁)

public class Singleton {
    private static Singleton instance;
    
    private Singleton() {}
    
    // 每次调用都加锁,性能低下
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

问题

  • synchronized 修饰整个方法,即使实例已创建,后续调用仍需竞争锁,吞吐量低。

三、Double-Checked Locking 的实现原理

3.1 基础实现(JDK 1.5 前有缺陷)

public class Singleton {
    private static Singleton instance; // 未声明 volatile
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        // 第一次检查:实例是否已创建,避免无意义的锁竞争
        if (instance == null) {
            synchronized (Singleton.class) { // 加锁范围缩小
                // 第二次检查:防止多个线程同时通过第一次检查
                if (instance == null) {
                    instance = new Singleton(); // 非原子操作,存在指令重排序问题
                }
            }
        }
        return instance;
    }
}

关键逻辑

  1. 第一次检查(instance == null:无锁状态下快速判断实例是否存在,避免大多数情况下的锁竞争。
  2. 加锁(synchronized:仅在实例未创建时对临界区加锁。
  3. 第二次检查:防止多个线程在等待锁时重复创建实例(如线程 A 释放锁后,线程 B 再次进入)。

3.2 指令重排序问题与 volatile 修正

问题根源
instance = new Singleton() 并非原子操作,实际分为 3 步:

  1. 分配内存空间(memory = allocate())。
  2. 初始化对象(ctorInstance(memory))。
  3. 将 instance 指向分配的内存(instance = memory)。
    指令重排序可能导致步骤 2 和 3 顺序颠倒,即:

  • 线程 A 执行到 instance = memory(未完成初始化)。
  • 线程 B 通过第一次检查(instance != null),直接返回未初始化的实例,导致 NPE 或逻辑错误。

修正方案
在 JDK 1.5 后,通过 volatile 关键字禁止指令重排序:

public class Singleton {
    // 声明 volatile 确保可见性和有序性
    private static volatile Singleton instance; 
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile 作用

  • 可见性:确保各线程读取到 instance 的最新值。
  • 有序性:通过 内存屏障(Memory Barrier) 禁止 instance 赋值与初始化操作的重排序。

四、模式的适用场景

4.1 单例模式的高性能实现

  • 适用场景
    • 单例实例创建成本较高(如初始化耗时、资源占用大)。
    • 系统中存在大量并发调用 getInstance() 的场景(如框架底层组件、配置管理器)。
  • 典型案例
    • Spring 框架中 ApplicationContext 的单例实现(部分场景)。
    • 数据库连接池、线程池的单例管理类。

4.2 非单例场景的扩展应用

  • 延迟初始化
    对任意类的实例化逻辑进行延迟加载,避免启动时创建所有对象:
    public class Resource {
        private volatile Data data; // 延迟加载的数据对象
        
        public Data getResource() {
            if (data == null) {
                synchronized (this) {
                    if (data == null) {
                        data = loadDataFromDisk(); // 耗时操作
                    }
                }
            }
            return data;
        }
    }
    

五、优缺点分析

5.1 优点

  1. 线程安全:通过双重检查和锁机制确保实例唯一性。
  2. 高性能:仅在第一次创建实例时加锁,后续调用无锁竞争,吞吐量显著高于全方法加锁。
  3. 懒加载:实例在首次使用时创建,减少启动时间。

5.2 缺点

  1. 代码复杂度:双重检查逻辑增加了代码理解难度,需严格遵循 volatile 和锁的使用规范。
  2. 适用限制
    • 仅适用于无状态或线程安全的单例对象(若单例包含可变状态,需额外保证状态的线程安全)。
    • 无法用于构造函数有副作用的场景(如构造函数中启动线程、注册监听器等)。

六、与其他单例模式的对比

单例模式实现方式线程安全性性能(多次调用)初始化时机代码复杂度
饿汉式(静态变量)安全高(无锁)类加载时简单
懒汉式(全方法加锁)安全低(每次加锁)首次调用时简单
Double-Checked Locking安全高(仅首次加锁)首次调用时复杂
枚举单例安全类加载时极简

选择建议

  • 简单场景:优先使用枚举单例(线程安全、防反射攻击)或饿汉式。
  • 高性能懒加载场景:使用 Double-Checked Locking(需注意 volatile)。

七、常见错误与最佳实践

7.1 常见错误

  1. 遗漏 volatile
    JDK 1.5 前的实现因指令重排序存在隐患,JDK 1.5 后若未声明 volatile,仍可能导致线程安全问题。
  2. 锁对象不一致
    错误使用非全局锁对象(如 this 或成员变量),导致不同线程持有不同锁:
    // 错误示例:锁对象为实例而非类,未加锁时实例未创建,锁对象不存在
    synchronized (instance) { ... } 
    

7.2 最佳实践

  1. 优先使用静态内部类单例
    利用类加载机制实现线程安全的懒加载,避免显式锁和 volatile
    public class Singleton {
        private Singleton() {}
        // 静态内部类持有单例实例,类加载时由 JVM 保证线程安全
        private static class Holder {
            static final Singleton INSTANCE = new Singleton();
        }
        public static Singleton getInstance() {
            return Holder.INSTANCE;
        }
    }
    
  2. 最小化锁范围
    确保 synchronized 仅包裹必要的代码块,避免在锁内执行耗时操作(如 I/O、网络请求)。
  3. 防御反射攻击
    在构造函数中添加防反射逻辑(仅适用于单例场景):
    private Singleton() {
        if (instance != null) {
            throw new IllegalStateException("Singleton instance already initialized");
        }
    }
    

八、总结

Double-Checked Locking 是多线程环境下实现高性能懒加载单例的经典模式,核心在于通过双重检查减少锁竞争和 **volatile 禁止指令重排序 **。其适用场景需满足:

  • 单例实例创建成本高且需懒加载。
  • 存在高并发调用需求。

实际开发中,若追求简单性和安全性,可优先选择枚举单例或静态内部类单例;若必须使用懒汉式且对性能敏感,则需严格遵循 volatile 和锁的规范,避免因指令重排序或锁使用不当导致的线程安全问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

潜意识Java

源码一定要私信我,有问题直接问

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值