跳转到内容

单例模式 (Singleton)

有的类在整个应用里只该存在一份:全局配置、连接池、某个内存缓存。如果到处 new,就会变成多份实例,既浪费又无法保证「大家用的是同一份」。

单例模式要做的事就是:限制构造、只留一个实例,并提供一个统一的取用方式。手写时一般是私有构造 + 静态持有实例 + getInstance();在基于容器的项目里,更常见的是用 Bean 的默认单例作用域,让「只建一次、到处注入同一个实例」由容器负责。


有些资源在语义上就是「全进程一份」:例如加载好的系统配置、一个连接池、一个简单的进程内缓存。如果在多个地方各自 new 一个「配置管理器」或「缓存」,就会变成多份,配置更新不同步、连接池被拆成好几块,行为会乱。所以需要两件事:禁止随意 new,以及提供一个统一的入口拿到那唯一一份实例

单例模式就是在类内部把「唯一实例」管起来,对外只暴露一个获取方法(如 getInstance()),调用方不关心实例何时创建、只关心拿到的是同一份。手写单例时还要考虑多线程:第一次创建时若两个线程同时进来,可能建出两个实例,因此要加锁或利用类加载只执行一次的特性。


二、思路:私有构造、唯一实例、统一入口

Section titled “二、思路:私有构造、唯一实例、统一入口”
  • 私有化构造函数:外部不能 new,只能通过类提供的静态方法获取实例。
  • 类内部持有唯一实例:用静态变量存这份实例。可以是「类加载时就创建」(饿汉),也可以是「第一次调用 getInstance 时再创建」(懒汉)。
  • 静态访问方法:例如 getInstance()。懒汉时在方法里判断实例是否已创建,未创建则创建并缓存;若多线程下只允许建一次,需要加锁或使用「静态内部类持有实例」(利用类加载的线程安全)。
  • 线程安全:饿汉和「静态内部类」都依赖类加载阶段只执行一次,天然线程安全;懒汉 + 双重检查锁时,实例变量需加 volatile,避免指令重排导致半初始化对象被看到。

单例类内部持有一个自己的实例,构造私有,对外只有获取方法。

classDiagram
    class Singleton {
        -instance Singleton
        -Singleton()
        +getInstance() Singleton
    }
    note for Singleton "唯一实例 全局访问点"

4.1 饿汉式(类加载即创建,线程安全)

Section titled “4.1 饿汉式(类加载即创建,线程安全)”

实例在类加载时就会创建,之后 getInstance() 直接返回,没有懒加载,但实现简单、线程安全。

public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}

4.2 懒汉式 + 双重检查锁(首次使用时创建,线程安全)

Section titled “4.2 懒汉式 + 双重检查锁(首次使用时创建,线程安全)”

第一次调用时才创建,多线程下用双重检查 + volatile 保证只建一次。

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;
}
}

4.3 静态内部类(懒加载 + 线程安全,常用)

Section titled “4.3 静态内部类(懒加载 + 线程安全,常用)”

实例放在静态内部类里,只有第一次访问 Holder.INSTANCE 时才会触发内部类的加载,从而创建实例;类加载由 JVM 保证线程安全。

public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}

五、在项目里:何时手写、何时用容器单例

Section titled “五、在项目里:何时手写、何时用容器单例”

手写 getInstance() 适合脱离容器的环境:工具库、某些 SDK、或没有依赖注入的入口。

在基于容器的 Web 应用中,默认的 Bean 作用域就是单例:容器只会创建一个实例,所有注入点拿到的都是同一个。所以「全局只该有一份」的组件,通常直接做成一个 Bean,不必自己写单例类;需要时注入即可,测试时也可以通过替换 Bean 做 Mock。

用一个进程内缓存举例:整个应用共用一份缓存实例,用 Bean 暴露,其它服务注入使用。这样「单例」由容器保证,代码里不出现 getInstance()

5.1 场景:应用内只该有一份的缓存

Section titled “5.1 场景:应用内只该有一份的缓存”

例如系统参数、开关、或简单统计的进程内缓存,希望全应用共享同一份,避免多处各建一份导致数据不一致。用一个「缓存持有者」类,以单例形式存在即可。

5.2 用 Bean 的单例作用域(推荐)

Section titled “5.2 用 Bean 的单例作用域(推荐)”

不手写单例,把该类交给容器管理;默认作用域就是单例,容器只会创建一个实例,注入处拿到的都是同一个。

package com.example.demo.singleton;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class InMemoryConfigCache {
private final Map<String, String> store = new ConcurrentHashMap<>();
public String get(String key) {
return store.get(key);
}
public void put(String key, String value) {
store.put(key, value);
}
public void invalidate(String key) {
store.remove(key);
}
}

其它服务或 Controller 需要用到这份缓存时,直接注入 InMemoryConfigCache,容器会注入同一个实例,无需 getInstance()

package com.example.demo.singleton;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ConfigService {
private final InMemoryConfigCache configCache;
public String getConfig(String key) {
String cached = configCache.get(key);
if (cached != null) return cached;
// 从库或远程拉取,再放入 configCache
return loadAndCache(key);
}
private String loadAndCache(String key) {
// ...
return null;
}
}

这样「全应用一份缓存」由容器的单例作用域保证,代码简单、也方便在测试里替换成别的 Bean。

5.3 若必须手写单例(如脱离容器的工具类)

Section titled “5.3 若必须手写单例(如脱离容器的工具类)”

不经过容器的代码里(例如一个被多处静态调用的工具类),不能依赖注入,只能手写单例。此时用静态内部类或饿汉即可,注意不要持有会随环境变化的依赖(如数据源),否则难以测试和配置。

public final class SomeUtil {
private static final class Holder {
private static final SomeUtil INSTANCE = new SomeUtil();
}
private SomeUtil() {}
public static SomeUtil getInstance() {
return Holder.INSTANCE;
}
}

  • 单例相当于全局状态:用多了会提高耦合,测试时若不能替换成 Mock,会难测。真正「全进程一份」的资源才适合单例;能通过参数或注入传递的,优先用注入。
  • 线程安全:手写懒汉时必须考虑多线程;饿汉和静态内部类依赖类加载,一般无需额外同步。Bean 单例由容器在启动阶段创建,通常也是安全的,但若单例内部有可变状态,读写仍需自己加锁或用线程安全结构。
  • 与容器单例的关系:在项目里,默认 Bean 就是单例;「单例模式」更多体现在「设计意图」——这类组件只该有一个实例。实现上交给容器即可,不必再写 getInstance()

单例通过私有构造和统一访问点,保证一个类只有一个实例,适合配置、连接池、进程内缓存等「全局一份」的场景。手写时可用饿汉、懒汉+双重检查锁或静态内部类,注意线程安全;在基于容器的项目中,直接用 Bean 的默认单例作用域即可,由容器保证只建一次、到处注入同一实例,代码更简单、也便于测试时替换。