怎么实现线程安全的单例模式?
线程安全的单例模式确保在多线程环境下,只有一个实例被创建。常见的实现方式包括 双重检查锁(DCL)、静态内部类、枚举 和 volatile + synchronized,每种方式通过同步机制或 JVM 特性保证线程安全。
关键事实
- 单例模式目标:
- 全局唯一实例,控制资源访问。
- 线程安全问题:
- 多线程并发创建实例,可能导致多个实例。
- 实现原则:
- 延迟加载(懒加载)或饿汉式。
- 同步控制或利用 JVM 类加载机制。
具体实现方式
1. 双重检查锁(Double-Checked Locking, DCL)
- 原理:结合 volatile 和 synchronized,延迟加载并确保线程安全。
- 特点:
- volatile 防止指令重排序,确保实例初始化完成。
- 双重检查减少锁开销。
2. 静态内部类
- 原理:利用 JVM 类加载机制,延迟加载,线程安全由类加载器保证。
- 特点:
- 类加载时单线程执行,天然线程安全。
- 懒加载,调用 getInstance 时才加载。
3. 枚举
- 原理:JVM 保证枚举实例唯一,防反射和序列化破坏。
- 特点:
- 最简洁,线程安全无额外开销。
- 不可继承,适合简单单例。
4. 饿汉式(静态初始化)
- 原理:类加载时创建实例,JVM 保证线程安全。
- 特点:
- 非懒加载,启动即创建。
- 简单但可能浪费资源。
5. synchronized 方法
- 原理:方法加锁,确保每次只有一个线程创建实例。
- 特点:
- 线程安全,但每次调用锁开销大。
延伸与面试角度
- DCL 为什么用 volatile?:
- 防止 instance = new Singleton() 的指令重排序(分配内存 → 初始化 → 赋值)。
- 无 volatile,线程可能拿到未初始化对象。
- 选择依据:
- DCL:懒加载 + 高性能,常用。
- 静态内部类:优雅,推荐。
- 枚举:防反射,简单场景。
- 饿汉式:启动快,无并发需求。
- 性能对比:
- DCL 和静态内部类优于同步方法(锁范围小)。
- 枚举和饿汉式无运行时开销。
- 反射破坏:
- 反射可通过构造器创建实例,枚举天然免疫。
- 其他方式需加防护:
- 实际应用:
- Spring Bean 默认单例,用 DCL 或类加载实现。
- 面试点:
- 问“线程安全原理”时,提同步或 JVM 类加载。
- 问“优缺点”时,比较懒加载和性能。
总结
线程安全的单例模式可用 DCL(高效懒加载)、静态内部类(优雅)、枚举(最安全)、饿汉式(简单)或同步方法实现。DCL 和静态内部类最常用,枚举防反射最佳。面试时,可写 DCL 代码并解释 volatile,展示深入理解。