Bootstrap

设计模式之 单例设计模式

单例设计模式(Singleton Design Pattern) 是软件开发中最常用的设计模式之一,属于 创建型设计模式。单例模式确保一个类在整个应用程序生命周期中只有一个实例,并提供一个全局访问点。


1. 什么是单例模式?

单例模式是一种设计模式,其目标是确保某个类只有一个实例,并为其他代码提供一个全局访问点。该模式主要解决的问题是 控制实例数量并提供统一的访问接口

单例模式的核心要素:
  1. 唯一实例:确保类的实例在应用程序中始终只有一个。
  2. 全局访问点:通过统一的接口访问该实例。
  3. 受控实例化:通过隐藏构造方法来控制实例化过程。

2. 单例模式的特点

  • 控制全局状态:单例类通常用于管理全局状态或资源(如数据库连接、线程池)。
  • 延迟加载(可选):可以延迟实例的创建,以减少资源开销。
  • 线程安全(需要额外处理):在多线程环境下,确保单例实例的创建和访问是线程安全的。
  • 对测试的影响:由于单例实例是全局的,它可能会对单元测试造成影响,需要通过依赖注入等方式规避。

3. 单例模式的实现方式

单例模式在 Java 中的实现可以分为以下几种方式:

3.1 懒汉式(Lazy Initialization)

懒汉式实现会在第一次需要单例实例时才进行创建,这种方式可以延迟初始化,但需要注意线程安全问题。

实现代码:
public class Singleton {
    private static Singleton instance;

    //构造方法私有化
    private Singleton(){}

    //提供公共接口
    public static Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}
优点:
  • 延迟初始化,节省资源。
缺点:
  • 非线程安全。在多线程环境下,可能会出现多个线程同时创建实例的情况。

3.2 线程安全的懒汉式

为了解决懒汉式的线程安全问题,可以对 getInstance() 方法加锁:

实现代码:
public class Singleton {
    private static Singleton instance;

    //构造方法私有化
    private Singleton(){}

    //提供公共接口
    public static synchronized Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}
优点:
  • 线程安全。
缺点:
  • 性能较差。synchronized 锁会导致每次调用 getInstance() 都需要加锁,即使实例已经创建。

3.3 双重检查锁(Double-Checked Locking)

双重检查锁是一种优化方案,通过减少锁的粒度来提升性能,仅在实例未初始化时加锁。

实现代码:
public class Singleton {
    private static volatile Singleton instance;
    private Singleton(){}

    public static Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
优点:
  • 线程安全。
  • 提高了性能,只有在实例未创建时才会加锁。
缺点:
  • 代码较复杂,需要使用 volatile 关键字来防止指令重排。

在 Java 中,volatile 关键字用来确保变量在多个线程间的可见性,并防止 JVM 对变量的读写进行指令重排。通过 volatile,可以确保读取到的变量是最新值,从而避免出现不一致的问题。 


3.4 饿汉式(Eager Initialization)

饿汉式在类加载时就创建实例,不存在线程安全问题,但会浪费资源。

实现代码:
public class Singleton {
    //类加载时实例化对象
    private static final Singleton instance = new Singleton();
    private Singleton(){}

    private static Singleton getInstance(){
        return instance;
    }
}
优点:
  • 实现简单。
  • 线程安全(类加载是线程安全的)。
缺点:
  • 提前创建实例,可能会造成资源浪费(如果实例很少被使用)。

3.5 静态内部类(推荐方式)

静态内部类方式结合了懒汉式和饿汉式的优点,既支持延迟加载,又是线程安全的。

实现代码:
public class Singleton {
    private Singleton(){}

    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton instance(){
        return SingletonHolder.INSTANCE;
    }
}
优点:
  • 线程安全。
  • 延迟加载,只有在第一次调用 getInstance() 时才会初始化。
  • 实现简单,推荐使用。

3.6 枚举实现(最佳方式)

使用枚举是实现单例模式的最佳方式,还能够避免反序列化和反射攻击。

实现代码:
public enum Singleton {
    instance;
    public Singleton getInstance(){
        return instance;
    }
}
优点:
  • 简单、线程安全。
  • 防止反射攻击:枚举类型在 JVM 层面天然防止反射创建新实例。
  • 防止反序列化:枚举类型的反序列化会返回同一个实例。
缺点:
  • 不够灵活,无法实现懒加载。

4. 单例模式的应用场景

单例模式广泛用于需要共享资源或全局管理的场景,以下是一些典型的应用:

  1. 配置管理

    例如,读取配置文件的类需要全局唯一,以便统一管理配置。
  2. 日志记录

    日志记录类通常设计为单例模式,以确保日志文件或输出流的唯一性。
  3. 数据库连接池

    数据库连接池是一个全局共享资源,通常用单例模式实现。
  4. 线程池管理

    线程池需要全局管理,防止重复创建和销毁线程资源。
  5. 操作系统资源管理

    如窗口管理器、文件系统、打印机管理器等。
  6. 缓存管理

    缓存类作为全局资源,确保所有模块共享相同的数据。

5. 单例模式的注意事项

  1. 线程安全

    在多线程环境中,懒汉式实现需要特别注意线程安全问题。
  2. 反序列化漏洞

    使用 Serializable 接口时,反序列化可能会创建新的实例。解决方法是重写 readResolve() 方法:
    protected Object readResolve() { 
        return getInstance(); 
    }
  3. 反射攻击

    反射可以通过私有构造方法创建多个实例。防御方法:
    • 在构造方法中检查是否已有实例
      private Singleton() { 
          if (instance != null) { 
              throw new RuntimeException("Instance already created"); 
          } 
      }
    • 使用枚举。
  4. 适用性

    单例模式适合需要全局唯一实例的场景,但滥用可能导致代码难以测试和扩展。
;