单例模式

单例模式

lx 0 2026-07-07

单利模式

核心目标非常明确:保证一个类在整个应用程序中只有一个实例,并提供一个全局访问点来获取它

核心原则:没有最好的模式,只有最适合场景的模式


一、为什么需要单例?

在以下场景中,一个类只需要一个实例:

  • 配置管理:整个系统共享一份配置信息(如数据库连接配置)。
  • 日志记录:所有模块都往同一个日志文件中写入。
  • 连接池:管理数据库连接,所有线程共享一个连接池。
  • Spring容器中的Bean:默认就是单例的(@Scope("singleton"))。

如果没有单例模式,多个地方 new出多个实例,会导致资源浪费和数据不一致。


二、单例模式的5种实现方式(Java)

单例模式的实现核心是:

  1. 私有化构造器,防止外部 new
  2. 提供一个静态方法,返回唯一的实例。

常见的实现方式有5种,我按推荐度从低到高排列:


方式1:饿汉式(线程安全,最简单)

public class Singleton {
    // 1. 类加载时就创建实例(静态变量)
    private static final Singleton INSTANCE = new Singleton();
  
    // 2. 私有化构造器
    private Singleton() {}
  
    // 3. 提供全局访问点
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

特点

  • 线程安全:由JVM类加载机制保证。
  • 简单:代码最少。
  • 立即加载:即使从未使用,类加载时也会创建,浪费内存(如果实例很重,如大对象)。

方式2:懒汉式(线程不安全,不推荐)

public class Singleton {
    private static Singleton instance;
  
    private Singleton() {}
  
    public static Singleton getInstance() {
        if (instance == null) {      // ❌ 多线程下会创建多个实例
            instance = new Singleton();
        }
        return instance;
    }
}

特点

  • 懒加载:需要时才创建。
  • 线程不安全:多个线程同时调用 getInstance()可能创建多个实例。

方式3:synchronized懒汉式(线程安全,但效率低)

public class Singleton {
    private static Singleton instance;
  
    private Singleton() {}
  
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

特点

  • 线程安全:方法加锁。
  • 懒加载
  • 效率低:每次调用都要获取锁,只有第一次创建时需要同步,后续应该直接返回实例。

方式4:双重检查锁(DCL,推荐)

这是实际开发中最常用的方式:

public class Singleton {
    // 1. volatile 防止指令重排序
    private static volatile Singleton instance;
  
    private Singleton() {}
  
    public static Singleton getInstance() {
        // 2. 第一次检查:避免不必要的同步
        if (instance == null) {
            // 3. 同步块:只锁定创建实例的代码
            synchronized (Singleton.class) {
                // 4. 第二次检查:防止多线程重复创建
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么需要 volatile

new Singleton() 在JVM中不是原子操作,可能发生指令重排序,导致其他线程拿到未初始化完全的对象。volatile 禁止了重排序,保证了可见性。

特点

  • 线程安全
  • 懒加载
  • 效率较高:只在第一次创建时同步。
  • ⚠️ 较复杂:需要理解 volatile和DCL的原理。

方式5:静态内部类(最优方案,强烈推荐)

利用JVM的类加载机制,既实现懒加载,又保证线程安全:

public class Singleton {
    // 私有构造器
    private Singleton() {}
  
    // 静态内部类(只有在调用 getInstance() 时才会加载)
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
  
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

特点

  • 线程安全:由JVM类加载机制保证。
  • 懒加载Holder类只在 getInstance()被调用时才加载,从而创建实例。
  • 代码简洁:比DCL更清晰。
  • 无性能损耗:没有同步锁。

三、反射和序列化攻击怎么办?

即使私有化构造器,反射仍可以强行调用构造器,破坏单例。

防御方式1:在构造器中检测并抛异常

public class Singleton {
    private static boolean isCreated = false;
  
    private Singleton() {
        if (isCreated) {
            throw new RuntimeException("单例模式禁止反射创建");
        }
        isCreated = true;
    }
}

防御方式2:使用 枚举(最安全)

枚举类型由JVM保证只会被实例化一次,即使反射也无法破坏:

public enum Singleton {
    INSTANCE;  // 单例对象
  
    // 可以添加方法
    public void doSomething() {
        System.out.println("Do something...");
    }
}

// 使用
Singleton.INSTANCE.doSomething();

为什么枚举最安全?

  • 枚举的构造器是JVM保护的,反射无法调用。
  • 序列化机制对枚举有特殊处理,不会创建新实例。

四、实际应用

1.Runtime.getRuntime()

// Java标准库源码(简化版)
public class Runtime {
    // 1. 饿汉式:类加载时即创建唯一实例
    private static final Runtime currentRuntime = new Runtime();
  
    // 2. 私有化构造器,禁止外部创建
    private Runtime() {}
  
    // 3. 全局访问点
    public static Runtime getRuntime() {
        return currentRuntime;
    }
  
    // 业务方法:获取处理器核心数
    public int availableProcessors() {
        return nativeAvailableProcessors();
    }
  
    // 业务方法:执行系统命令
    public Process exec(String command) throws IOException {
        return exec(command, null, null);
    }
}

为什么用饿汉式

Runtime 封装的是 JVM 的运行时环境。只要 JVM 启动,运行时环境就一定存在,这是毋庸置疑的。它不像某些业务工具类(如 DateFormat)可能永远用不到,需要延迟加载来节省资源。
既然无论如何都需要创建,那“立即加载”就不是缺点,反而是最符合逻辑的做法

2.Spring框架中的容器化单例:单例池(Singleton Pool)

Spring的单例不同于传统的“ClassLoader全局唯一”,它是“容器内唯一。每个Spring容器(ApplicationContext)维护一个单例池(Singleton Pool),通过 DefaultSingletonBeanRegistry 的核心缓存来管理。

核心数据结构:一级缓存

// DefaultSingletonBeanRegistry 源码(简化)
public class DefaultSingletonBeanRegistry {
    // 单例池:存放所有完全初始化好的单例Bean
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
  
    // 注册单例Bean
    protected void addSingleton(String beanName, Object singletonObject) {
        synchronized (this.singletonObjects) {
            this.singletonObjects.put(beanName, singletonObject);
            // 从其他缓存中移除...
        }
    }
  
    // 获取单例Bean
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        return this.singletonObjects.get(beanName);
    }
}

为什么Spring要设计自己的单例池

依赖注入(DI)生命周期管理等复杂场景 DCL等模式不适合,并不是为了单例而单例

五、三级缓存

1. 核心数据结构:三级缓存机制

DefaultSingletonBeanRegistry 内部维护了三个缓存,这是 Spring 单例管理的基石:

// 一级缓存:存放完全初始化好的单例 Bean(成品)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存:存放实例化完成但尚未填充属性的早期 Bean(半成品)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
// 三级缓存:存放 Bean 的工厂对象,用于生成早期引用
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

这三层缓存的设计,一方面保证了Bean获取的高效,另一方面也是Spring解决循环依赖问题的关键所在。

2. 核心流程:获取与创建

当你通过 getBean("myService") 获取一个单例Bean时,Spring的处理流程大致如下:

  1. 尝试获取:首先会调用 getSingleton(beanName) 方法,从上述三级缓存中尝试查找已创建好的实例。

    • 如果在 singletonObjects 中找到了,直接返回成品。
    • 如果没找到,但该Bean正在创建中,可能会从 earlySingletonObjectssingletonFactories 中获取早期引用(用于解决循环依赖)。
  2. 实例化与创建:如果缓存中不存在,Spring会进入创建流程,核心在 createBean()doCreateBean() 方法中。

    • 实例化:通过反射调用构造方法创建Bean实例(此时是半成品,属性还未注入)。
    • 提前暴露:将这个半成品的工厂对象存入三级缓存 singletonFactories,以便应对循环依赖。
    • 填充属性:执行 populateBean(),完成依赖注入。
    • 初始化:执行 initializeBean(),调用 @PostConstruct 等初始化方法。
    • 存入缓存:最终,将完全初始化好的Bean放入一级缓存 singletonObjects,并从其他缓存中移除。

3. 关键突破:循环依赖的解法

这是 Spring 单例管理的精髓。当A依赖B,B又依赖A时,Spring通过三级缓存巧妙化解:

  1. 实例化 A,将 A 的工厂对象放入三级缓存。
  2. 填充 A 的属性时,发现需要 B。
  3. 实例化 B,将 B 的工厂对象放入三级缓存。
  4. 填充 B 的属性时,发现需要 A。
  5. 此时,Spring 从三级缓存中获取 A 的工厂对象,生成一个A的早期引用(半成品),并将其提升到二级缓存。
  6. B 成功注入这个 A 的早期引用,完成初始化,B 成为成品进入一级缓存。
  7. A 继续完成初始化,最终也进入一级缓存。

注意:这种解决方式仅限于单例Bean的setter注入。构造器注入的循环依赖或原型Bean的循环依赖,Spring无法处理。在Spring Boot 2.6.x之后,循环依赖的默认行为也变成了“禁止”。

4. 核心区别一览

特性 Java 单例模式 Spring 单例 Bean
管理者 类自身(通过静态变量和私有构造器) IoC 容器(通过 DefaultSingletonBeanRegistry
唯一性范围 全局唯一(同一个 ClassLoader) 容器内唯一(不同容器可有不同实例)
生命周期控制 静态,由 JVM 控制 由容器全权管理,支持初始化和销毁回调
依赖注入 自身难以管理复杂依赖(通常需要组合其他单例) 原生支持,通过 @Autowired 等注解无缝注入
扩展能力 较弱,通常通过继承或接口扩展 极强,支持 AOP、代理、作用域切换(@Scope)等

简单来说,Java单例模式是一种“创造模式”,而Spring的单例Bean则是一种“管理服务”。后者在保证唯一性的基础上,解决了依赖注入、生命周期等更复杂的工程问题。