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

系列定位:软考软件设计师 / 系统架构设计师 --- 创建型模式专题第 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 staticgetInstance() 方法。


三、场景一:饿汉式单例(类加载即初始化,线程安全)

业务背景 :系统配置管理器 AppConfig,在应用启动时就需要加载配置文件,且整个生命周期只需一个实例。

说明:饿汉式的特点是 类加载时就完成实例化,借助 JVM 类加载机制保证线程安全,实现最简单,但可能造成资源浪费(如果实例从未被使用)。

3.1 代码实现
java 复制代码
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 代码实现(基础版,有线程安全问题)
java 复制代码
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 线程 B instance 状态
T1 判断 instance == null null
T2 判断 instance == null null
T3 执行 new ConnectionPool() null → 对象 A
T4 执行 new ConnectionPool() 对象 A → 对象 B

结果 :两个线程各自创建了一个实例,违反了单例原则。这是软考 高频考点


五、场景三:双重检查锁(DCL,延迟加载 + 线程安全)

业务背景 :高并发场景下的缓存管理器 CacheManager,需要延迟加载且保证线程安全,同时尽量减少同步开销。

说明:双重检查锁(Double-Checked Locking)是懒汉式的线程安全优化版,通过 两次判空 + 同步块 实现,既保证线程安全,又避免每次调用都加锁。

5.1 代码实现
java 复制代码
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 为什么必须用 volatile

instance = new CacheManager() 在 JVM 中并非原子操作,可能拆分为:

  1. 分配内存空间

  2. 初始化对象(执行构造器)

  3. 将引用指向内存地址

如果发生 指令重排序 (2 和 3 交换),其他线程可能在对象未完全初始化时就拿到引用,导致空指针异常。volatile 关键字禁止指令重排序,保证 happens-before 关系

软考要点 :如果代码填空考双重检查锁,实例字段必须加 volatile,否则存在隐患。


六、场景四:静态内部类(延迟加载 + 线程安全,推荐写法)

业务背景 :日志管理器 LogManager,要求延迟加载、线程安全,且实现简洁优雅。

说明:静态内部类方式是 Java 中 最推荐的单例实现 之一。利用 JVM 的类加载机制:外部类加载时不会加载内部类,只有调用 getInstance() 时才触发内部类加载,从而完成实例化。

6.1 代码实现
java 复制代码
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 代码实现
java 复制代码
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)。

java 复制代码
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)。

java 复制代码
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) 必须同时有 staticvolatile,缺一不可。如果只写 static 会扣分,因为缺少 volatile 无法防止指令重排序。


十一、常见陷阱与注意事项

陷阱 1:误认为单例是结构型模式

单例属于 创建型 模式,因为它控制对象的创建过程。软考上午题如果问"以下属于结构型模式的是",选项里出现"单例" 不能选

陷阱 2:懒汉式不加同步就直接用

懒汉式的 if (instance == null) { instance = new Xxx(); } 在多线程环境下 一定不安全。如果题目要求"线程安全",必须写 DCL 或静态内部类。

陷阱 3:DCL 忘记加 volatile

这是软考 最常考的填空陷阱 。DCL 中实例字段必须声明为 private static volatile Xxx instance,否则存在指令重排序隐患。阅卷时缺少 volatile 通常直接判错。

陷阱 4:构造器权限写错

单例的构造器必须是 private。如果写成 publicprotected,外部可以直接 new,单例就被破坏了。软考代码填空里这是送分点,但也是失分点。

陷阱 5:与 "静态类" 混淆

有人用全静态方法的类(如 public class Utils { public static void doXxx() {} })来替代单例,但这不是单例模式。单例模式的核心是 对象级别的唯一性,可以继承、可以实现接口、可以参与多态;静态类只是工具方法的集合。

陷阱 6:单例的序列化破坏

如果单例类实现了 Serializable,反序列化时会通过反射创建新实例,破坏单例。需要在类中重写 readResolve() 方法:

java 复制代码
protected Object readResolve() {
    return getInstance(); // 返回唯一实例
}

软考中此知识点出现频率较低,但选择题可能作为干扰项出现。


十二、总结

要点 内容
定义 确保一个类只有一个实例,并提供全局访问点
分类 创建型模式(GoF 23 正式成员)
核心三要素 private 构造器 ② private static 实例 ③ public static getInstance()
饿汉式 类加载时初始化,线程安全,无延迟加载
懒汉式(基础) 延迟加载,非线程安全
双重检查锁 延迟加载 + 线程安全,必须用 volatile
静态内部类 延迟加载 + 线程安全,无锁开销,最推荐
枚举 最简洁、最安全,防反射和序列化破坏
软考重点 代码填空补全三要素;判断线程安全性;DCL 中 volatile 的作用
答题技巧 看到"只有一个实例"、"全局访问点" → 单例;看到 private 构造器 + getInstance() → 确认单例

系列预告 :下一篇将讲 建造者模式 ------ 当对象的构造过程复杂、参数众多时,如何用"导演 + 建造者"的分工协作,一步步组装出复杂对象。咱们下回见。

相关推荐
折哥的程序人生 · 物流技术专研2 小时前
第4篇:模板方法 vs 策略模式,面试怎么选?
java·设计模式·策略模式·行为型模式·模版方法模式·扩充系列·继承v组合
小园子的小菜2 小时前
Java设计模式精讲:创建型+结构型模式(含生产级案例+相似模式对比)
学习·设计模式
geovindu20 小时前
java:Abstract Factory Pattern
java·开发语言·后端·设计模式·抽象工厂模式·创建型模式
geovindu20 小时前
java: Factory Method Pattern
java·开发语言·后端·设计模式·工厂方法模式·创建型模式
折哥的程序人生 · 物流技术专研1 天前
第2篇:模板方法模式核心:好莱坞原则
设计模式·uml·模板方法模式·行为型模式·钩子方法·扩充系列·好莱坞原则
折哥的程序人生 · 物流技术专研1 天前
第2篇:策略模式三大角色拆解与UML
java·设计模式·策略模式·uml·编程入门·行为型模式·扩充系列
Robot_Nav1 天前
C++ 面试常问问题汇总:基础语法、面向对象、STL、多线程与设计模式
c++·设计模式·面试
XX風1 天前
设计模式的本质
设计模式