设计模式-单例模式详解

单例模式详解

1. 模式初探:一个"唯一"的构想

想象一下,一家公司的CEO只能有一位,整个公司的战略决策都需要通过这唯一的角色来最终拍板。如果在代码世界中,我们需要一个类在任何情况下都只能拥有一个实例 ,并且这个实例要能提供一个全局的访问点,那么你就需要使用单例模式。

单例模式 是一种创建型模式,它确保一个类只有一个实例,并提供一个访问该实例的全局节点。

它要达到两个目的:

  • 控制实例数量:防止创建多个对象,节省系统资源。
  • 全局访问:为其他对象提供一个众所周知的访问点,避免在代码中频繁传递引用。

是 否 客户端 Client 调用 getInstance 实例是否存在? 返回现有实例 创建唯一实例 使用唯一实例的方法

2. 为何需要单例模式?优缺点权衡

在深入代码之前,我们先理性地看看单例模式的用武之地与需要留意之处。

应用场景

  • 全局配置管理器:系统的配置信息(如数据库连接字符串、应用设置)只需要一份,所有模块共享。
  • 连接池、线程池:池化资源的管理器需要是唯一的,以高效管理和分配资源。
  • 日志记录器:所有模块的日志都应该输出到同一个记录器实例,以保证日志格式和输出目标的一致。
  • 设备驱动程序对象 :比如打印机的PrintSpooler,多个打印任务应由同一个假脱机管理,避免冲突。

优点与缺点

优点 缺点
保证一个类只有一个实例,节省内存,特别是对于重量级对象。 对代码的侵入性强,违反了单一职责原则(类既要负责业务逻辑,又要控制实例化)。
提供了对该实例的全局访问点,方便获取。 隐藏了类之间的依赖关系,使代码耦合度变高,难以进行单元测试(Mock困难)。
仅在首次请求时初始化实例,可实现延迟初始化 多线程环境下需要特殊处理,否则可能创建多个实例。
可以严格控制访问,允许对操作进行精细化的控制。 过度使用可能导致它成为"全局状态",不利于模块化和重构。

关键认知:单例模式是一把双刃剑。在现代软件开发中,尤其是依赖注入框架(如Spring)普及后,许多传统的单例场景已被容器管理的"单例Bean"所取代。但理解其原理,对于处理遗留代码或某些特定场景(如工具类)依然至关重要。

3. Java实现:从基础到最佳实践

版本一:饿汉式(Eager Initialization)

"饿汉"很急切,类加载时就立即创建实例。

java 复制代码
public class SingletonEager {
    // 1. 静态私有成员,类加载时即初始化
    private static final SingletonEager INSTANCE = new SingletonEager();

    // 2. 私有构造函数,堵死外部通过`new`创建实例的路
    private SingletonEager() {
        System.out.println("Eager Singleton initialized.");
    }

    // 3. 静态公有方法,提供全局访问点
    public static SingletonEager getInstance() {
        return INSTANCE;
    }

    public void doSomething() {
        System.out.println("Doing something...");
    }
}

分析

  • 优点:实现简单,线程安全(由JVM类加载机制保证)。
  • 缺点不是延迟加载。即使应用从未使用这个单例,它也会被创建,可能造成资源浪费。

版本二:懒汉式(Lazy Initialization,线程不安全版)

"懒汉"不着急,等到第一次被调用时才创建。

java 复制代码
public class SingletonLazyUnsafe {
    private static SingletonLazyUnsafe instance;

    private SingletonLazyUnsafe() {}

    public static SingletonLazyUnsafe getInstance() {
        if (instance == null) { // 1. 第一次检查
            instance = new SingletonLazyUnsafe(); // 2. 创建实例 (危险区!)
        }
        return instance;
    }
}

分析

  • 优点:实现了延迟加载。
  • 致命缺点线程不安全 。如果两个线程同时通过第一次检查(instance == null),它们会先后创建两个不同的实例。

版本三:懒汉式(线程安全版,方法同步)

最直接的修复------给整个获取方法加锁。

java 复制代码
public class SingletonLazySync {
    private static SingletonLazySync instance;

    private SingletonLazySync() {}

    public static synchronized SingletonLazySync getInstance() {
        if (instance == null) {
            instance = new SingletonLazySync();
        }
        return instance;
    }
}

分析

  • 优点:简单,实现了线程安全的延迟加载。
  • 缺点性能差 。每次调用getInstance()都需要同步,而实际上只有第一次创建时需要锁。

版本四:双重检查锁定(Double-Checked Locking, DCL)

为了兼顾性能与延迟加载,DCL是经典解决方案。

java 复制代码
public class SingletonDCL {
    // 使用 volatile 关键字至关重要
    private static volatile SingletonDCL instance;

    private SingletonDCL() {
        System.out.println("DCL Singleton initialized.");
    }

    public static SingletonDCL getInstance() {
        if (instance == null) { // 第一次检查 (无需同步,性能关键)
            synchronized (SingletonDCL.class) { // 加锁
                if (instance == null) { // 第二次检查 (在锁内,确保安全)
                    instance = new SingletonDCL();
                }
            }
        }
        return instance;
    }
}

分析

  • volatile的作用 :防止指令重排。instance = new SingletonDCL(); 这行代码并非原子操作,可能发生重排序导致其他线程拿到一个未初始化完全的对象。volatile 禁止了这种重排,保证了可见性。
  • 优点:线程安全,延迟加载,且只有首次创建时需要同步,后续访问性能高。
  • 缺点 :实现稍复杂,需要理解volatile和内存屏障。

版本五:静态内部类(Holder)方式

利用JVM的类加载机制实现更优雅的懒加载。

java 复制代码
public class SingletonStaticHolder {
    private SingletonStaticHolder() {}

    // 静态内部类,持有单例实例
    private static class InstanceHolder {
        private static final SingletonStaticHolder INSTANCE = new SingletonStaticHolder();
    }

    public static SingletonStaticHolder getInstance() {
        // 首次调用getInstance时,才会加载InstanceHolder类并初始化INSTANCE
        return InstanceHolder.INSTANCE;
    }
}

分析

  • 原理 :JVM在加载外部类SingletonStaticHolder时,不会加载其内部类InstanceHolder。只有当调用getInstance()时,才会触发InstanceHolder的加载和静态变量INSTANCE的初始化,且这个过程由JVM保证线程安全。
  • 优点 :线程安全,实现简洁,延迟加载,且无需额外的同步开销。是目前最推荐的懒汉式实现之一。

版本六:枚举(Enum)方式

《Effective Java》作者Joshua Bloch极力推荐的方式。

java 复制代码
public enum SingletonEnum {
    INSTANCE; // 唯一的实例

    // 可以像普通类一样添加方法
    public void doSomething() {
        System.out.println("Enum Singleton working.");
    }
}
// 使用:SingletonEnum.INSTANCE.doSomething();

分析

  • 原理:Java的枚举类型在语言层面保证了每个枚举常量都是唯一的,且其初始化是线程安全的。
  • 优点
    1. 绝对的单例:能防止通过反射和反序列化创建多个实例(这是前几种方式需要额外处理的漏洞)。
    2. 代码极其简洁
    3. 线程安全,延迟初始化(枚举常量在首次被访问时初始化)。
  • 缺点 :不够灵活(例如无法继承其他类)。但在大多数单例场景下,这是最佳选择

4. 总结与选择建议

实现方式 线程安全 延迟加载 防止反射/反序列化 推荐度
饿汉式 ⭐⭐ 简单场景
懒汉式(同步方法) ⭐ 不推荐,性能差
双重检查锁定(DCL) ⭐⭐⭐ 需高性能延迟加载时
静态内部类 ⭐⭐⭐⭐ 经典优雅实现
枚举 ⭐⭐⭐⭐⭐ 生产环境首选

核心思想回顾 :单例模式的精髓在于将类的控制权交给类自身 。它通过私有化构造器,剥夺了外部通过new关键字创建对象的权力,并提供一个静态方法来返回其内部创建并持有的唯一实例。

在现代Spring Boot应用中,我们通常使用@Component@Service注解,并默认其作用域为singleton。这是框架提供的容器级单例,它管理了Bean的生命周期,并通常通过依赖注入(DI)的方式提供实例,这比传统手写单例模式更灵活、更易于测试,是更符合现代编程理念的做法。

相关推荐
未来之窗软件服务2 小时前
幽冥大陆(五十三)人工智能开发语言选型指南——东方仙盟筑基期
开发语言·人工智能·仙盟创梦ide·东方仙盟
踢球的打工仔2 小时前
jquery的基本使用(5)
前端·javascript·jquery
后端小张2 小时前
【JAVA 进阶】深入理解Sentinel:分布式系统的流量守卫者
java·开发语言·spring boot·后端·spring·spring cloud·sentinel
cheems95272 小时前
[JavaEE] CAS 介绍
java·开发语言·java-ee
想自律的露西西★2 小时前
js.39. 组合总和
前端·javascript·数据结构·算法
ttod_qzstudio2 小时前
事件冒泡踩坑记:一个TDesign Checkbox引发的思考
前端·javascript·vue.js·tdesign
zore_c2 小时前
【数据结构】栈——超详解!!!(包含栈的实现)
c语言·开发语言·数据结构·经验分享·笔记·算法·链表
lkbhua莱克瓦242 小时前
IO练习——登入注册
java·开发语言·io流·java练习题
Reuuse2 小时前
登录突然失效:Axios 拦截器判空、localStorage 脏数据与环境变量踩坑
开发语言·前端