【软考设计模式】单例模式:唯一实例的管控与线程安全精讲

【软考设计模式】单例模式:唯一实例的管控与线程安全精讲
系列定位软考软件设计师 / 系统架构设计师 — 创建型模式专题第 3 讲考察分值上午题 1-2 分下午题常作为代码填空或类图识别出现难度等级⭐⭐⭐☆☆概念简单但线程安全与实现细节陷阱极多一、考纲定位与模式定义1.1 考纲要求单例模式在软考中属于创建型模式的基础内容。考察形式包括上午选择题判断场景描述所属模式识别饿汉式、懒汉式、双重检查锁的实现差异判断线程安全性下午设计题补全单例类的私有构造器、静态实例字段、getInstance()方法识别类图中单例角色的典型特征私有构造器 静态获取方法1.2 模式定义单例模式确保某一个类只有一个实例并提供一个全局访问点来访问这个唯一实例。核心意图控制实例化过程让类自身负责保存它的唯一实例并阻止外部通过new随意创建。二、UML 类图与角色划分┌─────────────────────────────────────────────┐ │ Singleton │ │ - instance: Singleton │ │ - Singleton() │ │ getInstance(): Singleton │ │ businessMethod() │ └─────────────────────────────────────────────┘表格角色职责软考填空关键词Singleton单例类持有唯一实例提供全局访问点private static实例字段私有构造器阻止外部new创建private Singleton()静态获取方法返回唯一实例控制实例化时机public static getInstance()类图识别要点软考类图中单例类的构造器是private且包含一个指向自身的private static字段以及一个public static的getInstance()方法。三、场景一饿汉式单例类加载即初始化线程安全业务背景系统配置管理器AppConfig在应用启动时就需要加载配置文件且整个生命周期只需一个实例。说明饿汉式的特点是类加载时就完成实例化借助 JVM 类加载机制保证线程安全实现最简单但可能造成资源浪费如果实例从未被使用。3.1 代码实现public class AppConfig { // 1. 私有静态实例类加载时立即初始化 private static final AppConfig instance new AppConfig(); // 2. 私有构造器阻止外部 new private AppConfig() { System.out.println(AppConfig 初始化加载 application.properties); } // 3. 公共静态获取方法 public static AppConfig getInstance() { return instance; } // 业务方法 public String getProperty(String key) { return value-of- key; } } // 客户端 public class Client { public static void main(String[] args) { AppConfig config1 AppConfig.getInstance(); AppConfig config2 AppConfig.getInstance(); System.out.println(config1 config2); // true同一实例 System.out.println(config1.getProperty(db.url)); } }特点总结✅ 线程安全JVM 类加载机制保证✅ 实现简单无同步开销❌ 类加载即初始化无法延迟加载❌ 如果实例创建开销大且很少使用浪费资源四、场景二懒汉式单例延迟加载非线程安全业务背景数据库连接池管理器ConnectionPool只有在第一次真正需要连接时才初始化避免启动时占用资源。说明懒汉式的特点是第一次调用getInstance()时才创建实例实现了延迟加载但基础版本存在线程安全问题。4.1 代码实现基础版有线程安全问题public class ConnectionPool { // 1. 私有静态实例先不初始化 private static ConnectionPool instance; // 2. 私有构造器 private ConnectionPool() { System.out.println(初始化数据库连接池...); } // 3. 公共静态获取方法非线程安全 public static ConnectionPool getInstance() { if (instance null) { // 判断 1 instance new ConnectionPool(); // 创建 2 } return instance; } public void getConnection() { System.out.println(获取数据库连接); } }4.2 线程安全问题分析当两个线程同时执行getInstance()时时间线线程 A线程 Binstance 状态T1判断instance null✅nullT2判断instance null✅nullT3执行new ConnectionPool()null → 对象 AT4执行new ConnectionPool()对象 A → 对象 B结果两个线程各自创建了一个实例违反了单例原则。这是软考高频考点。五、场景三双重检查锁DCL延迟加载 线程安全业务背景高并发场景下的缓存管理器CacheManager需要延迟加载且保证线程安全同时尽量减少同步开销。说明双重检查锁Double-Checked Locking是懒汉式的线程安全优化版通过两次判空 同步块实现既保证线程安全又避免每次调用都加锁。5.1 代码实现public class CacheManager { // 1. 私有静态实例volatile 防止指令重排序 private static volatile CacheManager instance; // 2. 私有构造器 private CacheManager() { System.out.println(初始化缓存管理器...); } // 3. 双重检查锁 public static CacheManager getInstance() { // 第一次检查避免不必要的同步 if (instance null) { // 同步块保证线程安全 synchronized (CacheManager.class) { // 第二次检查防止多个线程穿透后重复创建 if (instance null) { instance new CacheManager(); } } } return instance; } public void put(String key, Object value) { System.out.println(缓存写入: key); } } // 客户端 public class Client { public static void main(String[] args) { CacheManager cache1 CacheManager.getInstance(); CacheManager cache2 CacheManager.getInstance(); System.out.println(cache1 cache2); // true cache1.put(user:1001, 张三); } }5.2 为什么必须用volatileinstance new CacheManager()在 JVM 中并非原子操作可能拆分为分配内存空间初始化对象执行构造器将引用指向内存地址如果发生指令重排序2 和 3 交换其他线程可能在对象未完全初始化时就拿到引用导致空指针异常。volatile关键字禁止指令重排序保证happens-before 关系。软考要点如果代码填空考双重检查锁实例字段必须加volatile否则存在隐患。六、场景四静态内部类延迟加载 线程安全推荐写法业务背景日志管理器LogManager要求延迟加载、线程安全且实现简洁优雅。说明静态内部类方式是 Java 中最推荐的单例实现之一。利用 JVM 的类加载机制外部类加载时不会加载内部类只有调用getInstance()时才触发内部类加载从而完成实例化。6.1 代码实现public class LogManager { // 1. 私有构造器 private LogManager() { System.out.println(初始化日志管理器...); } // 2. 静态内部类持有唯一实例 private static class SingletonHolder { private static final LogManager INSTANCE new LogManager(); } // 3. 公共静态获取方法 public static LogManager getInstance() { return SingletonHolder.INSTANCE; } public void log(String message) { System.out.println([LOG] message); } } // 客户端 public class Client { public static void main(String[] args) { LogManager log LogManager.getInstance(); log.log(系统启动成功); } }特点总结✅ 延迟加载第一次调用getInstance()时才加载内部类✅ 线程安全JVM 类加载机制保证✅ 无同步开销不需要synchronized✅ 实现简洁无volatile等复杂关键字七、场景五枚举单例最简洁、防反射、防序列化破坏业务背景系统全局计数器GlobalCounter要求绝对单例且防止通过反射或序列化破坏。说明枚举方式是 Java 中最安全的单例实现由 Joshua Bloch《Effective Java》作者推荐。它天然防止反射攻击和序列化破坏。7.1 代码实现public enum GlobalCounter { INSTANCE; // 唯一枚举常量就是单例实例 private int count 0; public void increment() { count; System.out.println(当前计数: count); } public int getCount() { return count; } } // 客户端 public class Client { public static void main(String[] args) { GlobalCounter counter GlobalCounter.INSTANCE; counter.increment(); // 当前计数: 1 counter.increment(); // 当前计数: 2 GlobalCounter another GlobalCounter.INSTANCE; System.out.println(counter another); // true } }为什么枚举最安全防反射枚举的构造器由 JVM 控制反射无法调用防序列化枚举的序列化机制只输出名称反序列化时直接返回INSTANCE常量不会创建新对象线程安全枚举实例的创建由 JVM 保证线程安全软考注意软考代码题通常用 Java 标准写法饿汉/懒汉/DCL枚举方式在选择题中可能作为最佳实践选项出现。八、五种实现方式对比维度饿汉式懒汉式基础双重检查锁静态内部类枚举初始化时机类加载时第一次调用第一次调用第一次调用类加载时线程安全✅ 安全❌ 不安全✅ 安全✅ 安全✅ 安全性能高无锁高无锁但不安全高细粒度锁高无锁高实现复杂度简单简单较复杂需volatile简单最简单延迟加载❌ 否✅ 是✅ 是✅ 是❌ 否防反射/序列化❌ 否❌ 否❌ 否❌ 否✅ 是软考出现频率⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐九、软考高频考点与易混淆辨析9.1 高频考点考点内容模式分类创建型模式GoF 23 正式成员核心三要素① 私有构造器 ② 私有静态实例 ③ 公共静态获取方法线程安全判断饿汉式安全类加载机制懒汉式不安全DCL 安全需volatile静态内部类安全DCL 的volatile必须加防止new操作的指令重排序导致半初始化对象暴露适用场景全局配置、连接池、缓存、日志、计数器等需要唯一实例的场景与工厂模式区别单例控制实例数量只有一个工厂控制创建逻辑创建哪种9.2 易混淆辨析单例 vs 简单工厂中的 唯一实例对比项单例模式简单工厂返回单例核心目的保证一个类只有一个实例工厂内部可能缓存实例但目的是创建对象实现方式私有构造器 静态获取方法工厂类内判断后返回new或缓存实例客户端调用Singleton.getInstance()Factory.createProduct()是否 GoF 模式✅ 是简单工厂 ❌ 不是 GoF 模式关键区分如果类图里只有一个类构造器私有有getInstance()→ 单例。如果类图里有工厂类、产品接口、多个产品实现 → 工厂模式。十、真题风格模拟与代码填空模拟题 1上午选择题以下关于单例模式的叙述中正确的是。A. 单例模式属于结构型模式B. 单例模式通过公共构造器确保只有一个实例C. 双重检查锁实现单例时实例字段必须声明为volatile以保证线程安全D. 懒汉式单例在类加载时就完成了实例化答案C解析A 错误单例属于创建型模式不是结构型。B 错误单例通过私有构造器阻止外部创建不是公共构造器。C 正确volatile防止new操作的指令重排序是 DCL 的必要条件。D 错误懒汉式是第一次调用时才初始化饿汉式才是类加载时初始化。模拟题 2下午代码填空某系统需要实现全局唯一的配置管理器ConfigManager使用单例模式实现。请补全1~4。public class ConfigManager { // 饿汉式类加载时初始化 private (1)______ ConfigManager instance (2)______; // 私有构造器 (3)______ ConfigManager() { System.out.println(配置管理器初始化); } // 全局访问点 public (4)______ ConfigManager getInstance() { return instance; } public String getConfig(String key) { return config- key; } }答案(1)static final(2)new ConfigManager()(3)private(4)static阅卷要点(1) 必须是static静态字段饿汉式通常加final(3) 必须是private这是单例的核心特征(4) 必须是static客户端通过类名调用。模拟题 3下午代码填空 — 双重检查锁请补全以下双重检查锁单例的1~3。public class TaskScheduler { private (1)______ TaskScheduler instance; private TaskScheduler() {} public static TaskScheduler getInstance() { if (instance null) { synchronized (TaskScheduler.class) { if ((2)______) { instance (3)______; } } } return instance; } }答案(1)static volatile(2)instance null(3)new TaskScheduler()阅卷要点(1) 必须同时有static和volatile缺一不可。如果只写static会扣分因为缺少volatile无法防止指令重排序。十一、常见陷阱与注意事项陷阱 1误认为单例是结构型模式单例属于创建型模式因为它控制对象的创建过程。软考上午题如果问以下属于结构型模式的是选项里出现单例不能选。陷阱 2懒汉式不加同步就直接用懒汉式的if (instance null) { instance new Xxx(); }在多线程环境下一定不安全。如果题目要求线程安全必须写 DCL 或静态内部类。陷阱 3DCL 忘记加volatile这是软考最常考的填空陷阱。DCL 中实例字段必须声明为private static volatile Xxx instance否则存在指令重排序隐患。阅卷时缺少volatile通常直接判错。陷阱 4构造器权限写错单例的构造器必须是private。如果写成public或protected外部可以直接new单例就被破坏了。软考代码填空里这是送分点但也是失分点。陷阱 5与 静态类 混淆有人用全静态方法的类如public class Utils { public static void doXxx() {} }来替代单例但这不是单例模式。单例模式的核心是对象级别的唯一性可以继承、可以实现接口、可以参与多态静态类只是工具方法的集合。陷阱 6单例的序列化破坏如果单例类实现了Serializable反序列化时会通过反射创建新实例破坏单例。需要在类中重写readResolve()方法protected Object readResolve() { return getInstance(); // 返回唯一实例 }软考中此知识点出现频率较低但选择题可能作为干扰项出现。十二、总结要点内容定义确保一个类只有一个实例并提供全局访问点分类创建型模式GoF 23 正式成员核心三要素①private构造器 ②private static实例 ③public staticgetInstance()饿汉式类加载时初始化线程安全无延迟加载懒汉式基础延迟加载非线程安全双重检查锁延迟加载 线程安全必须用volatile静态内部类延迟加载 线程安全无锁开销最推荐枚举最简洁、最安全防反射和序列化破坏软考重点代码填空补全三要素判断线程安全性DCL 中volatile的作用答题技巧看到只有一个实例、全局访问点 → 单例看到private构造器 getInstance()→ 确认单例系列预告下一篇将讲建造者模式—— 当对象的构造过程复杂、参数众多时如何用导演 建造者的分工协作一步步组装出复杂对象。咱们下回见。