单例模式

单例模式:设计模式中的全局唯一实例解决方案

一、单例模式的定义与核心意图

单例模式(Singleton Pattern)是创建型设计模式的典型代表,其核心目标可概括为两点:

  1. 实例唯一性:确保在整个应用程序的生命周期内,某个类只能创建出一个实例对象,避免因多实例导致的资源冲突或状态不一致问题。

  2. 全局可访问性:为该唯一实例提供一个统一的、全局的访问入口,无需频繁创建和销毁对象,降低系统性能开销。

从设计本质来看,单例模式通过控制对象的创建流程,将类的实例化权限 “收归己有”,从而

现对资源的集中管理。例如,数据库连接池、日志管理器等场景,若允许创建多个实例,不仅会浪费内存资源,还可能导致数据同步异常,这正是单例模式的核心应用价值所在。

二、单例模式的设计原理与 UML 类图

1. 核心设计原理

单例模式的实现依赖三个关键技术点,缺一不可:

  • 私有化构造方法:通过 private 修饰构造方法,禁止外部类通过 new 关键字直接创建实例,从根源上控制实例的创建渠道。实

  • 私有静态成员变量:在类内部定义一个 private static 修饰的成员变量,用于存储该类的唯一实例,确保实例与类本身绑定(而非与对象绑定)。

  • 公有静态访问方法:提供一个 public static 修饰的方法(通常命名为 getInstance()),作为全局访问入口,负责创建实例(若未创建)并返回该实例。

2. UML 类图

单例模式的类结构简洁清晰,以下是标准 UML 类图:

classDiagram class Singleton { - static instance: Singleton // 私有静态成员变量,存储唯一实例 - Singleton() // 私有化构造方法 + static getInstance(): Singleton // 公有静态方法,提供全局访问 + businessMethod(): void // 业务方法,实现具体功能 }

从类图可见,单例类不依赖其他类,仅通过自身静态成员和方法完成实例的控制与访问,符合 “高内聚” 的设计原则。

三、单例模式的多种实现方式详解

单例模式的实现方式多样,不同方式在懒加载、线程安全、性能等维度各有优劣,需根据实际场景选择。以下是常用实现方式的对比与代码示例:

1. 饿汉式(Eager Initialization)

核心特点

  • 初始化时机:类加载时(即 JVM 加载该类到内存时)立即创建实例,属于 “饿加载”,无需等待首次调用。

  • 线程安全:天生线程安全(JVM 保证类加载过程是线程安全的,且静态变量初始化仅执行一次)。

  • 性能:类加载阶段会占用内存,若实例未被使用,会造成资源浪费。

代码实现

public class EagerSingleton {

   // 1. 私有静态成员变量:类加载时直接初始化,确保唯一

   private static final EagerSingleton INSTANCE = new EagerSingleton();

  

   // 2. 私有化构造方法:禁止外部创建实例

   private EagerSingleton() {

       // 可选:防止通过反射破坏单例(后续章节详解)

       if (INSTANCE != null) {

           throw new RuntimeException("单例模式禁止重复创建实例");

       }

   }

  

   // 3. 公有静态访问方法:返回唯一实例

   public static EagerSingleton getInstance() {

       return INSTANCE;

   }

  

   // 业务方法示例

   public void printInfo() {

       System.out.println("饿汉式单例:当前实例哈希值=" + this.hashCode());

   }

}

适用场景

适合实例创建成本低、且程序启动后必然会使用的场景(如系统配置管理器)。

2. 懒汉式(Lazy Initialization)

核心特点

  • 初始化时机:首次调用 getInstance() 方法时才创建实例,属于 “懒加载”,避免资源浪费。

  • 线程安全:基础版线程不安全,需通过同步机制优化。

(1)基础版(线程不安全)

问题:多线程环境下,若多个线程同时进入 if (instance == null) 判断,会创建多个实例,破坏单例特性。

public class LazySingletonUnsafe {

   private static LazySingletonUnsafe instance;

  

   private LazySingletonUnsafe() {}

  

   // 线程不安全:多线程并发时可能创建多个实例

   public static LazySingletonUnsafe getInstance() {

       if (instance == null) {

           // 线程A、B同时进入此处,会创建两个实例

           instance = new LazySingletonUnsafe();

       }

       return instance;

   }

}

(2)同步方法版(线程安全但性能低)

优化:给 getInstance() 方法加 synchronized 关键字,确保同一时间只有一个线程执行该方法。

问题:每次调用 getInstance() 都会加锁,即使实例已创建,仍会产生锁竞争,导致性能下降。

public class LazySingletonSynchronized {

   private static LazySingletonSynchronized instance;

  

   private LazySingletonSynchronized() {}

  

   // 线程安全:但整个方法加锁,性能开销大

   public static synchronized LazySingletonSynchronized getInstance() {

       if (instance == null) {

           instance = new LazySingletonSynchronized();

       }

       return instance;

   }

}

(3)双重检查锁(DCL,推荐)

优化:将锁粒度缩小到 “实例创建代码块”,并通过两次 if (instance == null) 检查,兼顾线程安全与性能。

关键细节:需用 volatile 关键字修饰 instance,防止 JVM 指令重排(new 操作分三步:分配内存→初始化对象→赋值给变量,指令重排可能导致 “变量已赋值但对象未初始化”,其他线程获取到不完整实例)。

public class LazySingletonDCL {

   // volatile:防止指令重排,确保实例初始化完整后才被访问

   private static volatile LazySingletonDCL instance;

  

   private LazySingletonDCL() {

       // 防止反射破坏

       if (instance != null) {

           throw new RuntimeException("单例模式禁止重复创建实例");

       }

   }

  

   // 双重检查锁:高效且线程安全

   public static LazySingletonDCL getInstance() {

       // 第一次检查:实例已创建则直接返回,避免进入同步块(提升性能)

       if (instance == null) {

           // 类级别的锁:确保同一时间只有一个线程进入

           synchronized (LazySingletonDCL.class) {

               // 第二次检查:防止多个线程等待锁时重复创建实例

               if (instance == null) {

                   instance = new LazySingletonDCL();

               }

           }

       }

       return instance;

   }

}

适用场景

适合实例创建成本高、且不一定会被使用的场景(如大型文件解析器)。

3. 静态内部类(Static Inner Class)

核心特点

  • 懒加载:静态内部类 SingletonHolder 仅在 getInstance() 被调用时才会加载,避免资源浪费。

  • 线程安全:JVM 保证静态内部类的初始化过程是线程安全的,且静态变量 INSTANCE 仅初始化一次。

  • 性能:无需加锁,性能优于双重检查锁。

代码实现

public class InnerClassSingleton {

   // 1. 私有化构造方法

   private InnerClassSingleton() {

       // 防止反射破坏

       if (SingletonHolder.INSTANCE != null) {

           throw new RuntimeException("单例模式禁止重复创建实例");

       }

   }

  

   // 2. 静态内部类:仅在调用getInstance()时加载

   private static class SingletonHolder {

       // 静态变量:JVM保证初始化过程线程安全

       private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();

   }

  

   // 3. 公有静态访问方法

   public static InnerClassSingleton getInstance() {

       return SingletonHolder.INSTANCE;

   }

}

适用场景

大多数场景下的首选方案,兼顾懒加载、线程安全与性能。

4. 枚举单例(Enum Singleton)

核心特点

  • 《Effective Java》推荐:天然解决线程安全、反射破坏、序列化破坏三大问题。

  • 简洁性:代码量极少,无需手动处理复杂逻辑。

  • 局限性:无法实现懒加载(枚举类加载时即初始化实例)。

代码实现

public enum EnumSingleton {

   // 唯一实例(枚举常量默认是public static final的)

   INSTANCE;

  

   // 可选:添加成员变量存储状态

   private String config;

  

   // 枚举构造方法(默认private,且只能是private)

   EnumSingleton() {

       // 初始化操作:如加载配置文件

       this.config = "系统默认配置";

   }

  

   // 业务方法示例

   public void updateConfig(String newConfig) {

       this.config = newConfig;

   }

  

   public String getConfig() {

       return this.config;

   }

}

为什么枚举能防止反射与序列化?

  • 反射防护:JVM 明确规定,无法通过反射创建枚举类的实例(Constructor.newInstance() 会抛出 IllegalArgumentException)。

  • 序列化防护:枚举类默认实现 Serializable 接口,且 JVM 保证反序列化时不会创建新实例,而是直接返回已存在的枚举常量。

适用场景

对安全性要求极高、且可接受非懒加载的场景(如权限管理器、加密工具类)。

四、单例模式的安全性问题与防御措施

单例模式的 “唯一性” 可能被反射序列化破坏,需针对性防御:

1. 反射攻击与防御

反射破坏的原理

即使构造方法是 private,反射仍可通过 Constructor.setAccessible(true) 绕过访问权限检查,调用构造方法创建新实例。

破坏示例(以双重检查锁为例)

public class SingletonReflectionTest {

   public static void main(String[] args) throws Exception {

       // 1. 获取正常单例实例

       LazySingletonDCL instance1 = LazySingletonDCL.getInstance();

      

       // 2. 通过反射获取构造方法并创建新实例

       Class<LazySingletonDCL> clazz = LazySingletonDCL.class;

       Constructor<LazySingletonDCL> constructor = clazz.getDeclaredConstructor();

       constructor.setAccessible(true); // 绕过私有构造方法

       LazySingletonDCL instance2 = constructor.newInstance();

      

       // 3. 验证:两个实例不是同一个(单例被破坏)

       System.out.println(instance1 == instance2); // 输出 false

   }

}

防御措施

  • 方案 1:在构造方法中添加检查,若实例已存在则抛出异常(如前文代码中 “防止反射破坏” 的逻辑)。

  • 方案 2:使用枚举单例(JVM 天然防护,反射无法创建枚举实例)。

2. 序列化与反序列化问题

问题描述

若单例类实现 Serializable 接口,反序列化时(即从文件 / 网络读取对象时),JVM 会创建一个新实例,破坏单例特性。

破坏示例

public class SingletonSerializationTest {

   public static void main(String[] args) throws Exception {

       // 1. 序列化单例实例到文件

       SerializableSingleton instance1 = SerializableSingleton.getInstance();

       ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.obj"));

       oos.writeObject(instance1);

       oos.close();

      

       // 2. 从文件反序列化获取实例

       ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.obj"));

       SerializableSingleton instance2 = (SerializableSingleton) ois.readObject();

       ois.close();

      

       // 3. 验证:两个实例不是同一个(单例被破坏)

       System.out.println(instance1 == instance2); // 输出 false

   }

}

防御措施

在单例类中添加 readResolve() 方法,反序列化时会调用该方法返回已存在的实例,而非创建新实例:

public class SerializableSingleton implements Serializable {

   private static final long serialVersionUID = 1L; // 序列化版本号

  

   private static final SerializableSingleton INSTANCE = new SerializableSingleton();

  

   private SerializableSingleton() {}

  

   public static SerializableSingleton getInstance() {

       return INSTANCE;

   }

  

   // 关键:反序列化时返回已有实例

   private Object readResolve() {

       return INSTANCE;

   }

}

五、单例模式的应用场景

单例模式的核心价值是 “资源复用” 与 “状态统一”,以下是典型应用场景:

  1. 资源密集型对象:数据库连接池、线程池、Redis 连接客户端等。这类对象创建成本高(如建立网络连接),频繁创建会导致性能瓶颈,单例模式可减少资源消耗。

  2. 全局状态管理:系统配置管理器、日志管理器、用户会话管理器等。需确保全局只有一个 “数据源”,避免多实例导致的配置不一致或日志错乱。

  3. 硬件资源控制:打印机管理器、显卡驱动管理器等。硬件设备通常只能被一个实例控制,多实例可能导致设备冲突。

  4. 工具类优化:若工具类创建时需加载大量静态资源(如字典数据),单例模式可避免重复加载,提升效率。

六、单例模式的优缺点

优点

  1. 节省资源:控制实例数量为 1,避免多实例占用内存和 CPU 资源。

  2. 全局访问:提供统一入口,简化代码调用,无需在多个地方传递实例。

  3. 状态稳定:确保全局状态一致,避免多实例修改同一资源导致的并发问题。

缺点

  1. 违背单一职责原则:单例类既要实现业务逻辑,又要负责实例的创建与管理,职责过重。

  2. 扩展性差:若后续需求变更(如需要多个实例),需修改单例类的核心逻辑,不符合 “开闭原则”。

  3. 测试困难:单例类依赖全局状态,单元测试时难以模拟不同场景(如实例已初始化后无法重置)。

  4. 并发风险(实现不当):若未处理好线程安全(如使用基础版懒汉式),会导致多实例问题。

  5. 破坏依赖注入:单例类通常通过静态方法获取,而非通过构造方法注入,增加代码耦合度。

七、单例模式的变种

除了标准实现,单例模式还有一些变种,适用于特殊场景:

1. 容器式单例

核心思想

通过一个 “容器”(如 Map)管理多个单例对象,按 “key” 获取对应实例,实现 “多单例” 管理。

代码实现

public class SingletonContainer {

   // 容器:存储单例对象,key为类名或自定义标识

   private static final Map<String, Object> SINGLETON_MAP = new ConcurrentHashMap<>();

  

   private SingletonContainer() {}

  

   // 注册单例:若不存在则创建并加入容器

   public static <T> void registerSingleton(String key, Class<T> clazz) throws Exception {

       if (!SINGLETON_MAP.containsKey(key)) {

           T instance = clazz.getDeclaredConstructor().newInstance();

           SINGLETON_MAP.put(key, instance);

       }

   }

  

   // 获取单例:按key从容器中获取

   public static <T> T getSingleton(String key) {

       return (T) SINGLETON_MAP.get(key);

   }

}

适用场景

需管理多个单例对象的场景(如系统中有多个配置文件,每个配置文件对应一个单例)。

2. 线程局部单例(ThreadLocal Singleton)

核心思想

为每个线程创建一个实例,确保 “线程内唯一”,而非 “全局唯一”。适用于多线程场景下,每个线程需独立实例的需求(如线程上下文、事务管理)。

代码实现

public class ThreadLocalSingleton {

   // ThreadLocal:存储每个线程对应的实例

   private static final ThreadLocal<ThreadLocalSingleton> THREAD_LOCAL =

           ThreadLocal.withInitial(ThreadLocalSingleton::new);

  

   private ThreadLocalSingleton() {}

  

   // 获取当前线程的实例

   public static ThreadLocalSingleton getInstance() {

       return THREAD_LOCAL.get();

   }

  

   // 可选:移除当前线程的实例(避免内存泄漏)

   public static void removeInstance() {

       THREAD_LOCAL.remove();

   }

}

特点

  • 线程安全:每个线程操作自己的实例,无并发冲突。

  • 内存注意:需在线程结束时调用 removeInstance(),避免 ThreadLocal 导致的内存泄漏。

八、单例模式的最佳实践总结

  1. 优先选择枚举单例:若可接受非懒加载,枚举单例是最安全的方案(天然防反射、防序列化),且代码简洁。

  2. 需要懒加载选静态内部类:兼顾懒加载、线程安全与性能,是大多数场景的 “最优解”。

  3. 复杂场景用双重检查锁:若需在实例创建时传入参数(静态内部类无法直接传参),可使用双重检查锁,但需注意 volatile 关键字。

  4. 避免滥用单例:工具类若无需状态(如纯静态方法),无需设计为单例;简单对象(创建成本低)也无需单例。

  5. 处理特殊场景:需多单例用容器式,线程内唯一用 ThreadLocal,确保单例模式与需求匹配。

单例模式是设计模式中最基础也最容易被滥用的模式,关键在于 “权衡”—— 根据资源成本、并发需求、扩展性要求选择合适的实现方式,而非盲目使用。

posted @ 2025-11-23 10:39  圣祖帝皇  阅读(3)  评论(0)    收藏  举报