当前位置: 首页 > news >正文

JAVA手写题-精通 Java 单例模式:三种线程安全的实现方式详解

设计模式是软件开发中经过验证的、可重用的解决方案。其中,单例(Singleton)模式是最基本也最常用的模式之一。它的核心思想是确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点来获取这个实例。这在日志记录器、配置管理器、线程池等场景中非常有用。

实现单例模式看似简单,但在多线程环境下保证线程安全和高性能却需要仔细考虑。本文将深入探讨三种常用且线程安全的 Java 单例实现方式:枚举、静态内部类和双重校验锁(Double-Checked Locking)。

1. 枚举(Enum)—— 《Effective Java》推荐的最佳实践

Joshua Bloch 在其著作《Effective Java》中极力推荐使用枚举来实现单例。这种方式不仅代码简洁,而且由 JVM 从根本上保证了线程安全、防止反序列化重新创建新对象以及防止通过反射攻击。

/*** 使用枚举实现的单例模式* 优点:实现简单、线程安全、防止反序列化和反射攻击*/
public enum SingletonEnum {INSTANCE; // 定义一个枚举元素,它本身就是单例的实例// 可以添加其他方法public void doSomething(String str) {System.out.println("Enum Singleton doing: " + str);}// 示例用法public static void main(String[] args) {SingletonEnum singleton1 = SingletonEnum.INSTANCE;SingletonEnum singleton2 = SingletonEnum.INSTANCE;System.out.println(singleton1 == singleton2); // 输出 truesingleton1.doSomething("Hello World!");}
}

工作原理:

  • Java 枚举类型的实例是在类加载时由 JVM 保证唯一创建的。
  • JVM 保证了枚举构造器的线程安全。
  • 默认情况下,枚举实例的序列化和反序列化机制可以防止创建新对象。
  • 反射机制也无法通过构造器创建新的枚举实例。

优点:

  • 实现极其简单。
  • 天然线程安全,无需任何同步措施。
  • 有效防止反序列化和反射攻击。
  • 代码可读性高。

缺点:

  • 非懒加载(Lazy Loading),实例在枚举类加载时就被创建。但在大多数场景下,这不是问题。

结论: 如果你不需要延迟加载,枚举是实现单例模式的最简单、最安全的方式,强烈推荐。

2. 静态内部类(Static Inner Class)—— 兼顾线程安全与懒加载

静态内部类模式利用了 Java 类加载机制来保证线程安全和实现延迟加载。

/*** 使用静态内部类实现的单例模式* 优点:线程安全、懒加载、实现简单*/
public class SingletonStaticInner {// 1. 私有化构造方法private SingletonStaticInner() {System.out.println("SingletonStaticInner instance created.");}// 3. 定义静态内部类,持有单例实例private static class SingletonInner {// 在内部类中创建外部类实例,final确保不会被修改private static final SingletonStaticInner INSTANCE = new SingletonStaticInner();}// 2. 对外提供获取实例的公共静态方法public static SingletonStaticInner getInstance() {// 首次调用该方法时,才会加载SingletonInner类,并创建INSTANCEreturn SingletonInner.INSTANCE;}// 示例用法public static void main(String[] args) {System.out.println("Main method started.");// 只有调用getInstance()时,才会触发内部类的加载和实例的创建SingletonStaticInner instance1 = SingletonStaticInner.getInstance();SingletonStaticInner instance2 = SingletonStaticInner.getInstance();System.out.println(instance1 == instance2); // 输出 true}
}

工作原理:

  • 当外部类 SingletonStaticInner​ 被加载时,静态内部类 SingletonInner​ 并不会被立即加载。
  • 只有当第一次调用 getInstance()​ 方法访问 SingletonInner.INSTANCE​ 时,JVM 才会加载 SingletonInner​ 类。
  • 类的加载过程本身是线程安全的,JVM 会保证 INSTANCE​ 静态变量只被初始化一次。

优点:

  • 线程安全: 由 JVM 类加载机制保证。
  • 懒加载: 只有在第一次调用 getInstance()​ 时才创建实例。
  • 实现简单: 相较于双重校验锁更简洁。

缺点:

  • 仍然可能被反射攻击(可以通过在构造器中添加检查来防御)。

结论: 这是目前广泛使用的一种非常优秀的单例实现方式,兼顾了线程安全、懒加载和实现简洁性。

3. 双重校验锁(Double-Checked Locking, DCL)—— 兼顾性能与懒加载

双重校验锁旨在减少不必要的同步开销,以提高在高并发场景下获取实例的性能,同时实现懒加载。

/*** 使用双重校验锁实现的单例模式* 优点:线程安全、懒加载、性能较好(相比每次都同步)* 缺点:实现复杂,容易出错(尤其volatile关键字)*/
public class SingletonDCL {// 1. volatile 保证可见性和禁止指令重排序private volatile static SingletonDCL uniqueInstance;// 2. 私有化构造方法private SingletonDCL() {System.out.println("SingletonDCL instance created.");}// 3. 对外提供获取实例的公共静态方法public static SingletonDCL getUniqueInstance() {// 第一次检查:如果实例已存在,直接返回,避免进入同步块if (uniqueInstance == null) {// 同步块:确保只有一个线程能创建实例synchronized (SingletonDCL.class) {// 第二次检查:防止多个线程同时通过第一次检查后重复创建实例if (uniqueInstance == null) {// 创建实例uniqueInstance = new SingletonDCL();}}}return uniqueInstance;}// 示例用法public static void main(String[] args) {System.out.println("Main method started.");// 并发测试 (仅为示意,实际测试需要更复杂的工具)Thread t1 = new Thread(() -> System.out.println(SingletonDCL.getUniqueInstance()));Thread t2 = new Thread(() -> System.out.println(SingletonDCL.getUniqueInstance()));t1.start();t2.start();}
}

工作原理与 volatile​ 的关键性:

DCL 的核心在于两次 if (uniqueInstance == null)​ 检查和 synchronized​ 块。然而,volatile​ 关键字是 DCL 能够正确工作的关键。

正如你提供的解释中所述,uniqueInstance = new SingletonDCL();​ 这行代码并非原子操作,它大致分为三步:

  1. 分配内存空间:为 uniqueInstance​ 对象分配内存。
  2. 初始化对象:调用 SingletonDCL​ 的构造函数,进行初始化。
  3. 建立引用:将 uniqueInstance​ 变量指向分配好的内存地址。

没有 volatile​,JVM 的指令重排序可能导致执行顺序变为 1 -> 3 -> 2​。在多线程下:

  • 线程 T1 执行了步骤 1 和 3,uniqueInstance​ 此时非空,但对象未初始化。
  • 线程 T2 调用 getUniqueInstance()​,第一次检查 uniqueInstance == null​ 为 false​,直接返回了一个未完全初始化的对象。
  • 线程 T2 使用这个不完整的对象,可能导致程序错误。

​volatile​ 关键字通过以下两点解决了这个问题:

  1. 禁止指令重排序:确保初始化(步骤 2)一定在赋值(步骤 3)之前完成。
  2. 保证可见性:确保一个线程对 uniqueInstance​ 的修改(写入 volatile​ 变量)能立刻被其他线程看到(读取 volatile​ 变量)。

优点:

  • 线程安全: 正确实现(带 volatile​)是线程安全的。
  • 懒加载: 只有在需要时才创建实例。
  • 性能: 实例创建后,后续获取不再需要同步,理论上性能优于每次都同步的方法。

缺点:

  • 实现复杂: 相较于前两种方式更复杂,容易因忘记 volatile​ 或理解不清而出错。
  • 性能优势不明显: 现代 JVM 对 synchronized​ 优化得很好,DCL 的性能优势可能不如预期,尤其是在低并发或无竞争时。
  • 可能被反射攻击。

结论: DCL 是一种可行的方案,但实现较为复杂且容易出错。在现代 Java 中,除非有明确的性能瓶颈指向同步开销且无法使用前两种方式,否则一般不优先推荐 DCL。

总结与推荐

选择哪种单例实现方式取决于具体需求:

  1. 最佳选择(默认推荐):枚举(Enum)

    • 优点:最简单、最安全(线程、序列化、反射)、代码清晰。
    • 缺点:非懒加载。
  2. 优秀选择(懒加载场景):静态内部类(Static Inner Class)

    • 优点:线程安全、实现懒加载、代码相对简单。
    • 缺点:可能被反射攻击。
  3. 备选方案(复杂场景):双重校验锁(Double-Checked Locking)

    • 优点:线程安全、实现懒加载、理论上性能较好(避免后续同步)。
    • 缺点:实现复杂、易错(volatile​ 关键)、性能优势可能不明显、可能被反射攻击。

理解这三种单例模式的实现原理、优缺点和适用场景,能帮助你在实际开发中做出更明智的选择,编写出更健壮、更高效的代码。


相关文章:

  • JAVA:单例模式
  • 【锂电池剩余寿命预测】Transformer锂电池剩余寿命预测(Pytorch完整源码和数据)
  • Java : GUI
  • RC吸收电路参数设置实战
  • Python包的编译、构建与打包指南
  • IDEA常用快捷键及操作整理(详细图解,持续更新)
  • Allegro23.1新功能之如何冻结动态铜皮操作指导
  • 二、Web服务常用的I/O操作
  • 【Go语言】ORM(对象关系映射)库
  • 层级时间轮的 Golang 实现原理与实践
  • Grok发布了Grok Studio 和 Workspaces两个强大的功能。该如何使用?如何使用Grok3 API?
  • Win10安装 P104-100 驱动
  • Gin 框架中集成 runtime/debug 打印日志堆栈信息
  • Conda 虚拟环境复用
  • react的 Fiber 节点的链表存储
  • 通过示例学习:连续 XOR
  • 如何配置osg编译使支持png图标加载显示
  • mybatis首个创建相关步骤
  • 【音视频】SDL简介
  • 实验:串口通信
  • 影子调查丨危房之下,百余住户搬离梦嘉商贸楼
  • 一季度规模以上工业企业利润由降转增,国家统计局解读
  • 上海虹桥至福建三明直飞航线开通,飞行时间1小时40分
  • 时代邻里:拟收购成都合达联行科技剩余20%股权
  • 国家市场监管总局:民生无小事,严打民生领域侵权假冒违法行为
  • 成都一季度GDP为5930.3亿元,同比增长6%