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

深入理解 Java 单例模式:从基础到最佳实践

单例(Singleton)模式是 Java 中最基本、最常用的设计模式之一。它确保一个类在任何情况下都只有一个实例,并提供一个全局访问点来获取这个唯一的实例。

一、为什么需要单例模式?(使用场景)

单例模式主要适用于以下场景:

  1. 资源共享与控制访问:当实例需要共享资源(如数据库连接池、线程池、配置文件、日志对象)或者需要控制对共享资源的并发访问时,单例可以确保所有操作都通过这唯一的实例进行。
  2. 确保唯一实例:某些类在逻辑上只需要一个实例,例如代表应用程序配置的对象、硬件设备管理器等。多实例可能会导致状态不一致或资源冲突。
  3. 懒加载(Lazy Loading):在某些情况下,实例的创建可能比较耗费资源。如果该实例并非立即需要,可以通过懒加载的方式,在首次使用时才创建,从而减少程序启动时的资源消耗。

二、实现单例模式的核心要求

一个标准的单例模式通常包含以下三个要素:

  1. 私有的构造函数:防止外部代码通过 new​ 关键字直接创建类的实例。
  2. 私有的静态实例变量:在类的内部持有该类的唯一实例。
  3. 公有的静态方法:提供一个全局访问点(通常命名为 getInstance()​),用于获取类的唯一实例。

三、常见的单例实现方式

1. 饿汉式(Eager Initialization)

饿汉式在类加载时就直接创建实例,不管后续是否真的用到。

/*** 饿汉式单例* 优点:实现简单,线程安全(由JVM类加载机制保证)* 缺点:非懒加载,可能造成资源浪费*/
public class EagerSingleton {// 1. 私有静态final实例变量,在类加载时就初始化private static final EagerSingleton instance = new EagerSingleton();// 2. 私有构造方法private EagerSingleton() {// 防止外部实例化System.out.println("EagerSingleton instance created.");}// 3. 公有静态方法返回实例public static EagerSingleton getInstance() {return instance;}public void doSomething() {System.out.println("EagerSingleton is doing something.");}
}

核心思想:利用 JVM 的类加载机制保证实例创建的线程安全。当类被加载时,静态变量 instance​ 会被初始化,这个过程由 JVM 保证只执行一次。

关键点:

  • 线程安全:天然线程安全,无需额外加锁。
  • 非懒加载:实例在类加载时创建,即使从未调用 getInstance()​。
2. 懒汉式(Lazy Initialization)

懒汉式在首次调用获取实例的方法时才创建实例。需要特别注意线程安全问题。

方式一:简单同步方法(性能较低)

/*** 懒汉式单例 - 同步方法* 优点:懒加载* 缺点:线程安全但性能低(每次调用getInstance都有同步开销)*/
public class LazySingletonSynchronized {private static LazySingletonSynchronized instance;private LazySingletonSynchronized() {System.out.println("LazySingletonSynchronized instance created.");}// 使用synchronized保证线程安全,但锁定了整个方法public static synchronized LazySingletonSynchronized getInstance() {if (instance == null) {instance = new LazySingletonSynchronized();}return instance;}
}

方式二:双重校验锁(Double-Checked Locking, DCL)

为了提高性能,避免每次调用 getInstance()​ 都进行同步。

/*** 懒汉式单例 - 双重校验锁 (DCL)* 优点:懒加载,线程安全,性能相对较高* 缺点:实现复杂,需要volatile关键字防止指令重排序*/
public class LazySingletonDCL {// 1. 使用volatile关键字确保可见性和禁止指令重排序private static volatile LazySingletonDCL instance;private LazySingletonDCL() {System.out.println("LazySingletonDCL instance created.");}public static LazySingletonDCL getInstance() {// 第一次检查,避免不必要的同步if (instance == null) {// 同步块,保证只有一个线程创建实例synchronized (LazySingletonDCL.class) {// 第二次检查,防止多个线程重复创建if (instance == null) {// new操作非原子,volatile防止指令重排序问题instance = new LazySingletonDCL();}}}return instance;}
}

DCL 中 volatile​ 的重要性:
​instance = new LazySingletonDCL();​ 不是原子操作,可能分为三步:

  1. 分配内存空间。
  2. 初始化对象。
  3. 将 instance​ 指向分配的内存地址。
    JVM 可能进行指令重排序(如 1 -> 3 -> 2)。若无 volatile​,线程 A 执行 1 和 3 后,instance​ 非空但未初始化。线程 B 此时调用 getInstance()​,会跳过同步块直接返回未初始化的 instance​,导致错误。volatile​ 可禁止这种重排序并保证内存可见性。

方式三:静态内部类(推荐的懒汉式)

利用 JVM 类加载机制实现懒加载和线程安全,代码更简洁。

/*** 懒汉式单例 - 静态内部类* 优点:懒加载,线程安全(由JVM保证),实现简单*/
public class StaticInnerClassSingleton {private StaticInnerClassSingleton() {System.out.println("StaticInnerClassSingleton instance created.");}// 静态内部类private static class SingletonHolder {// 在内部类中持有实例,JVM保证初始化线程安全且只执行一次private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();}// 调用getInstance时,才会加载SingletonHolder,从而创建INSTANCEpublic static StaticInnerClassSingleton getInstance() {return SingletonHolder.INSTANCE;}
}

工作原理:

  • 外部类 StaticInnerClassSingleton​ 加载时,静态内部类 SingletonHolder​ 不会被加载。
  • 只有首次调用 getInstance()​ 方法访问 SingletonHolder.INSTANCE​ 时,JVM 才会加载 SingletonHolder​ 类,并初始化 INSTANCE​。
  • JVM 的类加载过程是线程安全的。

初始化时机对比:

特性静态变量(饿汉式)静态内部类(懒汉式)静态方法(懒汉同步方法/DCL)
初始化时机外部类初始化(加载)阶段首次主动使用内部类时首次主动调用 getInstance​ 时

核心:通过 JVM 类加载机制(饿汉式、静态内部类)实现的单例都是天然线程安全的,无需显式加锁。

3. 枚举式(Enum Singleton)—— 最佳实践

《Effective Java》作者 Joshua Bloch 推荐的方式,极其简洁、高效,并且天然防止反射和反序列化攻击。

/*** 枚举式单例 - 最佳实践* 优点:实现极简,线程安全,防止反射和反序列化攻击* 缺点:非传统意义上的懒加载(类加载时实例已备好,但枚举本身加载也可能延迟)*/
public enum EnumSingleton {INSTANCE; // 定义一个枚举元素,它本身就是单例实例// 可以添加业务方法public void doSomething() {System.out.println("EnumSingleton is doing something.");}// 示例用法public static void main(String[] args) {EnumSingleton s1 = EnumSingleton.INSTANCE;EnumSingleton s2 = EnumSingleton.INSTANCE;System.out.println(s1 == s2); // 输出 trues1.doSomething();}
}

为什么枚举是最佳实践?

  • 简洁性:代码量最少。
  • 线程安全:由 JVM 保证,无需担心。
  • 防止反序列化创建新实例:Java 枚举的序列化机制有特殊处理,readObject()​ 方法会直接返回已存在的枚举常量,而不是创建新对象。
  • 防止反射攻击:反射无法通过 newInstance()​ 方法创建枚举实例(会抛出异常)。
  • 避免资源浪费的误解:虽然枚举实例在类加载时就“准备好了”,但枚举类本身的加载也可能被延迟。更重要的是,枚举的设计意图明确,开发者使用时就是为了获取单例,几乎不存在“加载了但从未被调用”的浪费情况。相比之下,普通的饿汉式如果设计不当(例如被其他无关类意外触发加载),才可能造成实例创建后未被使用的浪费。JVM 对枚举也有内存优化。

四、破坏单例模式的途径及防御

1. 反射(Reflection)

反射可以通过调用私有构造函数来创建新的实例。

破坏示例(假设有 LazySingletonDCL​ 类):

Constructor<LazySingletonDCL> constructor = LazySingletonDCL.class.getDeclaredConstructor();
constructor.setAccessible(true); // 强行访问私有构造
LazySingletonDCL instance1 = LazySingletonDCL.getInstance();
LazySingletonDCL instance2 = constructor.newInstance(); // 创建新实例
System.out.println(instance1 == instance2); // 输出 false

防御:在私有构造函数中增加检查,如果实例已存在,则抛出异常。

private LazySingletonDCL() {// 防止反射创建新实例if (instance != null) {throw new RuntimeException("Singleton instance already exists. Use getInstance() method.");}System.out.println("LazySingletonDCL instance created.");
}

image

(注意:枚举天然防御反射)

2. 反序列化(Deserialization)

如果单例类实现了 Serializable​ 接口,通过反序列化 readObject()​ 方法会创建一个新的实例。

防御:在单例类中添加 readResolve()​ 方法。该方法在反序列化时被调用,如果存在,其返回值会取代 readObject()​ 返回的对象。我们让它直接返回现有的单例实例。

// 在实现了Serializable的单例类中添加:
protected Object readResolve() {return getInstance(); // 返回当前唯一的实例
}

image

(注意:枚举天然防御反序列化)

五、不使用锁实现线程安全的单例

回顾一下,以下方式无需在 getInstance()​ 中使用 synchronized​ 也能保证线程安全:

  1. 饿汉式:利用 JVM 类加载时初始化静态变量的线程安全性。
  2. 静态内部类:利用 JVM 加载内部类时初始化静态变量的线程安全性。
  3. 枚举式:由 JVM 从语言层面保证其唯一性和线程安全性。

六、示例:小明的购物车

下面是你提供的购物车示例代码,它使用了 DCL 实现单例。

import java.util.*;
import java.io.*;// 使用DCL实现的购物车单例
class ShoppingCart {// volatile 保证可见性和禁止指令重排序private static volatile ShoppingCart instance;// 注意:将购物车内容设为静态,意味着所有用户共享同一个购物车列表!// 在真实场景中,购物车通常与用户会话关联,而不是全局单例。// 但作为单例模式的演示,这里保持原样。private static List<String> productsNames = new ArrayList<>();private static List<Integer> produtsQuatities = new ArrayList<>();// 私有构造private ShoppingCart() {System.out.println("ShoppingCart instance created.");};// DCL 获取实例public static ShoppingCart getInstance() {if (instance == null) {synchronized (ShoppingCart.class) {if (instance == null) {instance = new ShoppingCart();}}}return instance;}// 添加商品到共享列表public void add(String name, int quantity) {productsNames.add(name);produtsQuatities.add(quantity);System.out.println("Added to cart: " + name + " " + quantity);}// (可以添加其他方法,如展示购物车内容等)
}public class Main {public static void main(String[] args) {// 获取唯一的购物车实例ShoppingCart cart = ShoppingCart.getInstance();Scanner sc = new Scanner(System.in);String inputLine;System.out.println("Enter product name and quantity (e.g., 'apple 5'), type 'exit' to quit:");while (sc.hasNextLine()) {inputLine = sc.nextLine();if ("exit".equalsIgnoreCase(inputLine.trim())) {break;}String[] parts = inputLine.trim().split("\\s+"); // 使用正则匹配一个或多个空格if (parts.length == 2) {String name = parts[0];try {int quantity = Integer.parseInt(parts[1]);if (quantity > 0) {// 通过单例实例添加商品cart.add(name, quantity);} else {System.out.println("Quantity must be positive.");}} catch (NumberFormatException e) {System.out.println("Invalid quantity format. Please enter a number.");}} else {System.out.println("Invalid input format. Please enter 'name quantity'.");}}sc.close();System.out.println("Exiting program.");// (可以添加展示最终购物车内容的代码)}
}

注意:这个购物车示例将商品列表设为 static​。这意味着无论程序中有多少用户(理论上,即使有多个线程调用 getInstance()​),他们操作的都是同一个商品列表。在真实应用中,购物车通常是每个用户一个实例(可能存放在 Session 中),而不是全局单例。但作为演示单例模式的例子,它展示了如何获取和使用唯一的 ShoppingCart​ 对象。

七、总结

单例模式是保证类只有一个实例的重要工具。选择哪种实现方式取决于具体需求:

  • 追求极致简洁、安全:优先选择 枚举式。
  • 需要懒加载且希望实现简单:静态内部类 是非常好的选择。
  • 需要懒加载且有历史代码或特殊性能考虑:可以使用 DCL,但务必确保 volatile​ 的正确使用。
  • 不介意非懒加载或实例创建成本低:饿汉式 最简单直接。


相关文章:

  • 【项目篇之垃圾回收】仿照RabbitMQ模拟实现消息队列
  • 查回来的数据除了 id,其他字段都是 null
  • 自然语言处理之机器翻译:注意力机制在低资源翻译中的突破与哲思
  • LeetCode每日一题4.27
  • 【dockerredis】用docker容器运行单机redis
  • C#中属性和字段的区别
  • pytorch搭建并训练神经网络
  • Golang 遇见 Kubernetes:云原生开发的完美结合
  • MPI Code for Ghost Data Exchange in 3D Domain Decomposition with Multi-GPUs
  • 20250427 对话1: 何东山的宇宙起源理论
  • vscode eslint与vue-official冲突,导致点击的时候鼠标不会变成手型,一直在加载,但是不转到相应方法。
  • vue2 项目的 vscode 插件整理
  • Marmoset Toolbag 5.0 中文汉化版 八猴软件中文汉化版 免费下载
  • Maven 依赖范围(Scope)详解
  • 写windows服务日志-.net4.5.2-定时修改数据库中某些参数
  • 批量级负载均衡(Batch-Wise Load Balance)和顺序级负载均衡(Sequence-Wise Load Balance)
  • 【如何使用solidwork编辑结构导入到simscope】
  • FastAPI中的依赖注入详解与示例
  • MLLM之Bench:LEGO-Puzzles的简介、安装和使用方法、案例应用之详细攻略
  • 语音合成之八-情感化语音合成的演进路线
  • 巴防长称中俄可参与克什米尔恐袭事件国际调查,外交部回应
  • 广东一公司违规开展学科培训被罚没470万,已注销营业执照
  • “天链”继续上新!长三乙火箭成功发射天链二号05星
  • 伊朗外长:美伊谈判进展良好,讨论了很多技术细节
  • 泰山景区管委会:未经审核同意不得擅自举办竞速类登山活动
  • 《2025职场人阅读报告》:超半数会因AI改变阅读方向