文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

深入浅出设计模式【一、单例模式】

一、单例模式介绍

单例模式是一种创建型设计模式,其核心在于确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。

在软件系统中,经常存在这样的需求:一个类只需要一个实例来协调系统行为。例如,线程池、缓存、日志对象、对话框、打印机驱动对象等。如果这些类存在多个实例,会导致程序行为异常、资源使用过量,或者结果不一致。单例模式正是为解决此类问题而生的。

二、核心概念与意图

  1. 核心概念

    • 唯一实例: 控制实例的数量,禁止通过其他方式(如 new)创建该类的对象。
    • 全局访问点: 提供一个众所周知的静态方法,让任何需要该实例的客户端代码都能轻松获取到它。
  2. 意图

    • 保证一个类仅有一个实例
    • 提供对该实例的全局访问点

三、适用场景剖析

单例模式不应滥用,其典型适用场景包括:

  1. 控制资源访问: 当需要严格控制和统一管理共享资源时。例如,数据库连接池、线程池、缓存系统等。多个实例会导致资源耗尽或状态混乱。
  2. 管理全局状态: 当需要一个对象来存储整个应用程序的全局状态或配置信息时。例如,应用的配置管理器(ConfigManager),所有组件都应从同一个地方读取配置。
  3. 充当中央通信枢纽: 当对象需要充当一个中央通信点,协调多个不同部分的操作时。例如,日志记录器(Logger),所有模块都将日志信息发送到同一个日志器实例进行处理和输出。
  4. 工厂对象本身: 如果工厂类本身不包含需要变化的状态,它也可以被设计成单例,例如抽象工厂模式中的具体工厂类。

不适用场景

  • 如果对象本身是有状态的且状态可能随客户端变化,使用单例会导致意外的副作用(隐含的耦合)。
  • 在需要大量测试的场景中,单例的全局状态会使单元测试变得困难,因为它难以被隔离和模拟(Mock)。

四、UML 类图解析

单例模式的UML类图非常简单,但内涵明确。

Singleton
-static Singleton instance
-Singleton()
+static Singleton getInstance()
+void someBusinessLogic()
  • Singleton
    • 私有静态成员 (- static instance: Singleton): 用于保存类的唯一实例。
    • 私有构造函数 (- Singleton()): 这是实现单例的关键。将构造函数设为私有,防止外部代码通过 new Singleton() 的方式创建实例。这是对“控制实例创建”的直接体现。
    • 公有静态方法 (+ static getInstance(): Singleton): 这是全局访问点。该方法负责检查实例是否已存在。如果不存在,则创建它;如果已存在,则返回已有的实例。这是对“提供全局访问点”的直接体现。

五、各种实现方式及其优缺点

单例的实现方式多样,其演进史也反映了对线程安全、性能、语义清晰度的不断追求。

1. 饿汉式 (Eager Initialization)

在类加载时就立即初始化实例。

public class EagerSingleton {
    // 1. 私有静态实例,类加载时即初始化
    private static final EagerSingleton INSTANCE = new EagerSingleton();

    // 2. 私有构造函数
    private EagerSingleton() {}

    // 3. 公有静态方法,返回唯一实例
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}
  • 优点
    • 实现简单
    • 线程安全: 利用类加载机制保证线程安全(JVM在加载类时是互斥的)。
  • 缺点
    • 可能造成资源浪费: 如果这个实例非常耗费资源,但在整个程序生命周期中从未被使用过,就造成了不必要的开销。
    • 无法延迟加载 (Lazy Loading)

2. 懒汉式 (Lazy Initialization) - 非线程安全版

只有在第一次调用 getInstance() 时才创建实例。

public class UnsafeLazySingleton {
    private static UnsafeLazySingleton instance;

    private UnsafeLazySingleton() {}

    public static UnsafeLazySingleton getInstance() {
        // 非线程安全:多线程环境下可能创建多个实例
        if (instance == null) {
            instance = new UnsafeLazySingleton();
        }
        return instance;
    }
}
  • 优点
    • 实现了延迟加载
  • 缺点
    • 线程不安全: 在多线程环境下,多个线程可能同时进入 if (instance == null) 判断,从而创建多个实例。绝对不可用于生产环境

3. 懒汉式 - 线程安全版 (Synchronized Method)

通过给方法加锁来解决线程安全问题。

public class SynchronizedLazySingleton {
    private static SynchronizedLazySingleton instance;

    private SynchronizedLazySingleton() {}

    // 使用 synchronized 关键字,保证方法级别的同步
    public static synchronized SynchronizedLazySingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedLazySingleton();
        }
        return instance;
    }
}
  • 优点
    • 线程安全
    • 实现了延迟加载
  • 缺点
    • 性能低下: 每次调用 getInstance() 都需要进行同步,而大部分情况下实例已经创建,同步就成了不必要的开销。

4. 双重校验锁 (Double-Checked Locking - DCL)

在加锁前后进行两次检查,以减少同步的开销。

public class DCLSingleton {
    // 使用 volatile 关键字禁止指令重排序,这是JDK5之后的关键修正
    private static volatile DCLSingleton instance;

    private DCLSingleton() {}

    public static DCLSingleton getInstance() {
        // 第一次检查,避免不必要的同步
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                // 第二次检查,确保在同步块内实例仍为null
                if (instance == null) {
                    instance = new DCLSingleton(); // 1. 分配内存 2. 初始化 3. 引用赋值
                    // volatile 防止步骤2和3被重排序,保证其他线程看到的是完全初始化的对象
                }
            }
        }
        return instance;
    }
}
  • 优点
    • 线程安全
    • 延迟加载
    • 性能较高: 只有在第一次创建实例时才会同步。
  • 缺点
    • 实现稍复杂
    • 需要理解 volatile 和指令重排序的内存模型概念

5. 静态内部类 (Static Inner Class / Holder Class)

利用JVM的类加载机制来实现懒加载和线程安全,这是推荐的一种实现方式

public class InnerClassSingleton {
    private InnerClassSingleton() {}

    // 静态内部类
    private static class SingletonHolder {
        private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }

    public static InnerClassSingleton getInstance() {
        // 首次调用getInstance()时,JVM才会加载SingletonHolder类并初始化INSTANCE
        return SingletonHolder.INSTANCE;
    }
}
  • 优点
    • 线程安全: 由JVM保证。
    • 延迟加载: 只有在调用 getInstance() 时,内部类才会被加载,实例才会被创建。
    • 实现简洁: 无需 synchronizedvolatile,代码优雅。
  • 缺点
    • 无法防止通过反射或反序列化创建新实例。

6. 枚举 (Enum)

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

public enum EnumSingleton {
    INSTANCE; // 唯一的实例

    // 可以添加方法
    public void doSomething() {
        System.out.println("Doing something with " + this);
    }
}

// 使用:EnumSingleton.INSTANCE.doSomething();
  • 优点
    • 绝对防止多次实例化: 即使面对复杂的反射和序列化攻击,也能保证单例。这是由JVM在底层保证的。
    • 线程安全
    • 实现极其简单
  • 缺点
    • 不适用于需要继承的场景(因为枚举隐式继承自 Enum)。
    • 不够灵活(例如无法实现延迟加载,虽然这在枚举单例中通常不是问题)。

六、最佳实践

  1. 首选枚举实现: 如果你的场景适用(不需要延迟加载、不继承其他类),枚举是实现单例的最佳方式,因为它简单、安全、无敌。
  2. 次选静态内部类实现: 如果需要延迟加载,并且不介意潜在的反射/反序列化漏洞,静态内部类是实现单例的最佳方式。它简洁、高效、安全。
  3. 明确需求: 在决定使用单例前,反复问自己是否真的只需要一个实例。单例本质上是一种“优雅的全局变量”,要警惕其对代码可测试性和耦合性的影响。
  4. 考虑依赖注入 (DI): 在现代框架(如Spring)中,容器管理的Bean默认就是单例。优先使用框架的IoC容器来管理单例,而不是自己实现。这样既能享受单例的好处,又能避免手动实现的问题,并且便于测试。
  5. 防御反射和序列化攻击
    • 反射: 在私有构造函数中添加检查,如果实例已存在则抛出异常。
    private Singleton() {
        if (instance != null) {
            throw new IllegalStateException("Instance already exists");
        }
    }
    
    • 序列化: 如果单例类实现了 Serializable,必须提供 readResolve() 方法以防止反序列化时创建新对象。
    protected Object readResolve() {
        return getInstance();
    }
    

七、在开发中的演变和应用

单例模式的概念随着编程范式和技术的发展而演变:

  1. 从手动实现到容器托管: 早期Java开发中,开发者需要手动实现上述各种单例。如今,在Spring等IoC框架中,通过 @Component@Bean 注解,并指定作用域为 singleton,容器会自动创建并管理单例Bean的生命周期。这是单例思想在架构层面的应用和升华。
  2. 单例与多例: 单例模式催生了“多例模式”(Multiton)的概念,即一个类有有限个实例(如连接池中的多个连接),并通过一个键(Key)来获取。
  3. 与工具类的区别: 工具类(Math, Arrays)只有静态方法,没有状态,不需要实例化。单例类通常有状态(如配置信息、缓存数据),虽然只有一个实例,但它是一个真正的对象。

八、真实开发案例(Java语言内部、知名开源框架、工具)

  1. Java Runtime 类

    Runtime runtime = Runtime.getRuntime();
    

    Runtime 类封装了Java应用运行时的环境,每个Java应用程序都有一个唯一的 Runtime 实例。这是典型的饿汉式单例。

  2. Java Desktop 类

    Desktop desktop = Desktop.getDesktop();
    

    用于与桌面环境交互,如打开文件、打印等。也是单例。

  3. Spring Framework
    Spring容器中,默认情况下,由容器创建和管理的Bean都是单例的。这是单例模式在企业级开发中最广泛、最成功的应用。

    @Service // 默认就是单例
    public class UserServiceImpl implements UserService {
        // ...
    }
    
  4. 日志框架
    Logback / Log4j 中的 Logger 实例通常按名称(通常是类名)来获取,对同一个名称的 Logger 的获取总是返回同一个实例,这本质上是一种单例注册表(Registry)的实现,类似于多例模式。

    Logger logger = LoggerFactory.getLogger(MyClass.class);
    
  5. 工具类
    虽然工具类本身不是单例模式(因为它们没有实例),但其思想与单例“全局唯一访问点”的意图相通。例如 java.util.Collections

九、总结

方面总结
模式类型创建型设计模式
核心意图保证一个类只有一个实例,并提供全局访问点
关键实现1. 私有构造函数
2. 私有静态实例引用
3. 公有静态获取方法
主要优点1. 严格控制实例数量,节约系统资源
2. 提供一致的全局访问点,避免混乱
主要缺点1. 违反了单一职责原则(既管理实例又承担业务)
2. 代码耦合性高,不利于测试和扩展
3. 在分布式或并行环境中,可能需要特殊处理
适用场景需要严格管理共享资源或全局状态的场景,如配置、连接池、日志、工厂等
实现推荐简单安全首选枚举,延迟加载首选静态内部类。现代开发中优先使用IoC容器托管。

单例模式是一个简单而强大的模式,但其“全局状态”的特性是一把双刃剑。在现代软件开发中,应谨慎使用手动实现的单例,更多地将其思想与依赖注入容器相结合,从而在获得单例好处的同时,保持代码的松散耦合和可测试性。

posted @ 2025-08-29 12:46  NeoLshu  阅读(3)  评论(0)    收藏  举报  来源