【从零入门23种设计模式02】创建型之单例模式(5种实现形式)

作者:逆境不可逃

注意:文章内容参考网络,根据个人理解输出,如若帮助到你,请多多点赞关注谢谢!

一、单例模式核心定义

单例模式是保证一个类在整个应用程序生命周期中只有一个实例对象,并提供一个全局唯一的方法来获取这个实例的设计模式。

通俗比喻:单例模式就像 "公司的唯一 CEO"------ 整个公司只有一位 CEO,所有员工想对接 CEO,都只能通过唯一的渠道(比如秘书)找到他,不可能出现多个 CEO。核心目标:

  1. 控制实例数量:避免频繁创建 / 销毁实例造成的资源浪费;
  2. 全局统一访问:确保所有代码使用的是同一个实例,避免多实例导致的状态不一致(比如配置类、连接池)。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

二、单例模式的常见实现方式(Java)

单例模式的实现核心是:私有化构造方法 (防止外部通过new创建实例) + 提供静态方法获取实例,按 "实例初始化时机" 分为两大类,以下是最常用的 5 种实现:

1. 饿汉式(立即加载,天然线程安全)

核心逻辑:类加载时就创建实例("饿" 意味着迫不及待初始化),JVM 类加载机制保证线程安全(类加载过程是单线程的)。

复制代码
public class SingletonHungry {
    // 1. 私有化构造方法:禁止外部new
    private SingletonHungry() {}

    // 2. 类加载时直接创建实例(饿汉式核心)
    private static final SingletonHungry INSTANCE = new SingletonHungry();

    // 3. 提供全局访问方法
    public static SingletonHungry getInstance() {
        return INSTANCE;
    }
}

特点

  • 优点:实现简单、天然线程安全,无并发问题;
  • 缺点:类加载时就初始化,若实例一直未使用,会浪费内存(比如大型资源类)。
2. 懒汉式(基础版,线程不安全)

核心逻辑 :调用getInstance()时才创建实例("懒" 意味着按需加载),但多线程下会创建多个实例。

复制代码
public class SingletonLazyUnsafe {
    private SingletonLazyUnsafe() {}

    // 初始为null,调用时才创建
    private static SingletonLazyUnsafe instance;

    public static SingletonLazyUnsafe getInstance() {
        if (instance == null) { // 多线程下此处会并发穿透,创建多个实例
            instance = new SingletonLazyUnsafe();
        }
        return instance;
    }
}

特点

  • 优点:延迟加载,节省内存;
  • 缺点:线程不安全,仅适合单线程场景,多线程下会破坏 "单例" 特性。
3. 懒汉式(同步方法版,线程安全但低效)

核心逻辑 :给getInstance()synchronized锁,解决线程安全问题,但每次调用方法都要加锁,性能差。

复制代码
public class SingletonLazySafe {
    private SingletonLazySafe() {}

    private static SingletonLazySafe instance;

    // 加synchronized保证线程安全
    public static synchronized SingletonLazySafe getInstance() {
        if (instance == null) {
            instance = new SingletonLazySafe();
        }
        return instance;
    }
}

特点

  • 优点:线程安全、延迟加载;
  • 缺点:锁粒度太大,每次获取实例都要加锁,高并发场景下性能损耗大。
4. 双重检查锁(DCL,推荐!延迟加载 + 线程安全 + 高效)

核心逻辑 :两次检查instance是否为空(第一次不加锁,第二次加锁),结合volatile防止指令重排,是工业级常用方案。

复制代码
public class SingletonDCL {
    private SingletonDCL() {}

    // volatile:防止JVM指令重排(关键!否则可能拿到未初始化完成的实例)
    private static volatile SingletonDCL instance;

    public static SingletonDCL getInstance() {
        // 第一次检查:不加锁,快速判断,避免每次加锁(提升性能)
        if (instance == null) {
            synchronized (SingletonDCL.class) { // 类锁,仅第一次创建时加锁
                // 第二次检查:防止多线程同时走到第一个if后,重复创建
                if (instance == null) {
                    instance = new SingletonDCL();
                }
            }
        }
        return instance;
    }
}

特点

  • 优点:延迟加载、线程安全、高效(仅第一次创建时加锁);
  • 注意:volatile必须加 ------new SingletonDCL()会被 JVM 拆分为 "分配内存→初始化→赋值",不加volatile可能导致指令重排,其他线程拿到 "赋值但未初始化" 的实例。
5. 枚举单例(最优!天然线程安全 + 防反射 / 序列化破坏)

核心逻辑:利用 Java 枚举的特性(枚举类的实例是 JVM 保证的单例,且无法通过反射创建),是《Effective Java》推荐的最佳单例实现。

复制代码
public enum SingletonEnum {
    // 唯一枚举实例,等同于饿汉式的INSTANCE
    INSTANCE;

    // 枚举类的业务方法(可选)
    public void doSomething() {
        System.out.println("枚举单例执行业务逻辑");
    }
}

// 使用方式:SingletonEnum.INSTANCE.doSomething();

import java.io.*;
import java.util.HashMap;
import java.util.Map;

// 枚举单例:全局配置管理器(实际开发中常用场景)
public enum ConfigManager {
    // 1. 唯一枚举实例(JVM保证全局唯一,无法被创建多个)
    INSTANCE;

    // 2. 配置存储(模拟应用的核心配置:数据库地址、接口密钥等)
    private final Map<String, String> configMap = new HashMap<>();

    // 3. 初始化配置(枚举实例创建时执行,相当于饿汉式的初始化)
    ConfigManager() {
        // 模拟从配置文件/环境变量加载配置
        configMap.put("db.url", "jdbc:mysql://localhost:3306/test");
        configMap.put("db.username", "root");
        configMap.put("api.key", "1234567890abcdef");
        System.out.println("配置管理器初始化完成");
    }

    // 4. 业务方法:获取配置(对外提供的核心功能)
    public String getConfig(String key) {
        return configMap.getOrDefault(key, "配置不存在");
    }

    // 5. 业务方法:修改配置(可选,保证全局配置统一修改)
    public void updateConfig(String key, String value) {
        configMap.put(key, value);
        System.out.println("配置更新:" + key + " = " + value);
    }

    // --------------------- 测试枚举单例的核心优势 ---------------------
    public static void main(String[] args) throws Exception {
        // ********** 测试1:验证单例特性(多次获取都是同一个实例)**********
        ConfigManager config1 = ConfigManager.INSTANCE;
        ConfigManager config2 = ConfigManager.INSTANCE;
        System.out.println("config1 == config2:" + (config1 == config2)); // 输出:true
        // 验证配置一致性
        System.out.println("config1获取db.url:" + config1.getConfig("db.url")); // 输出:jdbc:mysql://localhost:3306/test
        config1.updateConfig("db.url", "jdbc:mysql://localhost:3306/prod");
        System.out.println("config2获取db.url:" + config2.getConfig("db.url")); // 输出:jdbc:mysql://localhost:3306/prod(配置统一)

        // ********** 测试2:验证防反射攻击(无法通过反射创建枚举实例)**********
        try {
            // 获取枚举类的构造方法(枚举的构造方法是私有的,且参数固定为(String, int))
            Class<ConfigManager> clazz = ConfigManager.class;
            java.lang.reflect.Constructor<ConfigManager> constructor = clazz.getDeclaredConstructor(String.class, int.class);
            constructor.setAccessible(true);
            // 尝试通过反射创建新实例(会直接抛出异常)
            ConfigManager fakeInstance = constructor.newInstance("FAKE", 1);
        } catch (Exception e) {
            System.out.println("反射创建枚举实例失败:" + e.getMessage()); // 输出:Cannot reflectively create enum objects
        }

        // ********** 测试3:验证防序列化破坏(序列化/反序列化后仍是同一个实例)**********
        // 序列化枚举实例
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("config.ser"));
        oos.writeObject(ConfigManager.INSTANCE);
        oos.close();
        // 反序列化枚举实例
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("config.ser"));
        ConfigManager deserializedInstance = (ConfigManager) ois.readObject();
        ois.close();
        System.out.println("反序列化后实例 == 原实例:" + (deserializedInstance == ConfigManager.INSTANCE)); // 输出:true
    }
}

特点

  • 优点:① 天然线程安全,JVM 保证枚举实例唯一;② 防止反射破坏:反射无法创建枚举实例;③ 防止序列化破坏:枚举序列化 / 反序列化后仍是同一个实例;
  • 缺点:无法延迟加载(枚举类加载时就创建实例),但几乎不影响,是最安全的单例实现。
6. 登记单例(注册式 / 容器式单例,线程安全 + 延迟加载)

核心逻辑 :通过全局线程安全的 Map(注册表)统一管理多个单例类的实例,实例在首次注册 / 获取时创建(延迟加载),所有单例类无需单独实现创建逻辑,由容器统一管控;利用ConcurrentHashMap保证多线程下注册 / 获取的原子性,避免重复创建实例。通俗比喻:登记单例就像 "应用的单例管家"------ 所有需要单例的类都在管家的 "花名册(Map)" 里登记,要用时找管家拿,管家保证每个类只给你一个实例,不用每个类自己管 "单例" 这件事。

复制代码
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

// 登记单例核心容器类
public class SingletonRegistry {
    // 1. 私有化构造方法:禁止外部new容器实例
    private SingletonRegistry() {}

    // 2. 线程安全的ConcurrentHashMap作为单例注册表(核心)
    private static final Map<String, Object> SINGLETON_CONTAINER = new ConcurrentHashMap<>();

    // 3. 注册/获取单例实例(核心方法:不存在则创建,存在则直接返回)
    public static <T> T getInstance(Class<T> clazz) {
        if (clazz == null) {
            throw new IllegalArgumentException("目标类类型不能为空");
        }
        String key = clazz.getName(); // 用类全限定名作为唯一key
        
        // 利用ConcurrentHashMap的原子性操作,避免多线程重复创建
        SINGLETON_CONTAINER.putIfAbsent(key, newInstance(clazz));
        
        // 类型转换后返回实例
        return clazz.cast(SINGLETON_CONTAINER.get(key));
    }

    // 私有工具方法:通过反射创建类实例(需保证目标类有可访问的默认构造方法)
    private static <T> T newInstance(Class<T> clazz) {
        try {
            return clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("创建" + clazz.getName() + "单例实例失败", e);
        }
    }
}

// 测试用业务类(无需自己实现单例逻辑)
class ConfigUtil {}
class LogUtil {}

// 测试登记单例使用
public class SingletonRegistryTest {
    public static void main(String[] args) {
        // 获取ConfigUtil单例
        ConfigUtil config1 = SingletonRegistry.getInstance(ConfigUtil.class);
        ConfigUtil config2 = SingletonRegistry.getInstance(ConfigUtil.class);
        System.out.println(config1 == config2); // 输出:true(单例)

        // 获取LogUtil单例
        LogUtil log1 = SingletonRegistry.getInstance(LogUtil.class);
        LogUtil log2 = SingletonRegistry.getInstance(LogUtil.class);
        System.out.println(log1 == log2); // 输出:true(单例)
    }
}
特点
  • 优点:① 统一管理多个单例类,无需为每个类编写重复的单例逻辑,代码冗余低;② 延迟加载(首次获取时创建实例),节省内存;③ 线程安全(依赖ConcurrentHashMap的原子操作);④ 扩展性强,新增单例类只需直接调用getInstance,无需修改容器代码;⑤ 是 Spring BeanFactory、MyBatis SqlSessionFactory 等框架的核心实现思路。
  • 缺点:① 依赖反射创建实例,若目标类私有化了默认构造方法,则无法创建;② 注册表(Map)若被恶意修改(如移除实例),会破坏单例特性;③ 逻辑比基础单例模式复杂,轻量场景(仅 1-2 个单例类)使用性价比低。
总结
  1. 核心逻辑:通过 "线程安全的容器(Map)" 统一注册、管理多个单例类,实现实例的延迟创建和全局唯一访问;
  2. 核心优势:适配多单例类管理场景,降低代码冗余,是框架级单例管理的主流方式;
  3. 核心限制:依赖反射、逻辑稍复杂,轻量场景无需使用。

三、单例模式的优缺点

优点
  1. 资源复用:仅创建一个实例,避免频繁创建 / 销毁(比如数据库连接池、线程池);
  2. 状态统一:全局使用同一个实例,避免多实例导致的配置 / 数据不一致;
  3. 简化访问:提供全局唯一入口,无需到处传递实例。
缺点
  1. 违反单一职责原则:单例类既负责业务逻辑,又负责实例的创建和管理;
  2. 扩展困难:单例类私有化构造方法,无法通过继承扩展;
  3. 多线程调试困难:全局实例的状态可能被多线程修改,难以追踪问题;
  4. 内存泄漏风险:若单例持有外部对象(如 Context)的强引用,长期不释放会导致内存泄漏。

四、实际应用场景

  1. JDK 内置java.lang.Runtime(每个 JVM 进程只有一个 Runtime 实例,通过Runtime.getRuntime()获取);
  2. Spring 框架:Spring 容器中的 Bean 默认是单例(BeanFactory 创建的 Bean 只有一个实例);
  3. 工具类:日志工具(如 log4j 的 Logger)、配置工具(全局配置类)、缓存工具;
  4. 资源控制:数据库连接池、线程池、打印机驱动(避免多实例抢占硬件资源)。

五、总结

  1. 核心目标:保证类只有一个实例,提供全局唯一访问入口;
  2. 实现关键:私有化构造方法 + 静态方法获取实例;
  3. 选型建议
    • 简单场景(无需延迟加载):饿汉式;
    • 需延迟加载 + 高性能:双重检查锁(DCL);
    • 追求绝对安全(防反射 / 序列化):枚举单例;
  4. 线程安全:饿汉式、DCL、枚举天然线程安全,基础懒汉式线程不安全。
相关推荐
37手游后端团队19 小时前
全网最简单!从零开始,轻松把 openclaw 小龙虾装回家
人工智能·后端·openai
用户83071968408219 小时前
Spring Boot WebClient性能比RestTemplate高?看完秒懂!
java·spring boot
Apifox19 小时前
测试数据终于不用到处复制了,Apifox 自动化测试新增「共用测试数据」
前端·后端·测试
Gardener17219 小时前
OpenStack Instance ID 映射机制详解
后端
用户938169125536020 小时前
Head First 模板方法模式
设计模式
无责任此方_修行中20 小时前
拒绝 AI 焦虑!一个普通程序员的真实 AI 工作流(附成本账单)
后端·程序员·ai编程
Assby21 小时前
从洋葱模型看Java与Go的设计哲学:为什么它们如此不同?
java·后端·架构
命运石之门的选择21 小时前
Flink 并行度调优"黄金三步法"
后端
泰式大师21 小时前
在 AI Agent 场景下,我们如何优雅地处理长文本?
后端