设计模式:单例模式

欢迎你来读这篇博客,这篇博客主要是关于单例模式
其中包括单例模式的核心思想、适用场景、常见实现方式、线程安全问题、反射与序列化破坏问题,以及 Java 后端开发中的真实案例。

序言

单例模式应该是设计模式里最容易“看懂”,但也最容易“写错”的模式之一。

很多人第一次接触单例模式时,觉得它不过就是:

一个类只能创建一个对象。

这句话没错,但只说对了一半。

在真实 Java 项目中,单例模式真正要考虑的问题包括:

  • 如何保证全局只有一个实例;
  • 如何控制对象创建时机;
  • 如何保证多线程环境下安全;
  • 如何避免反射破坏单例;
  • 如何避免序列化和反序列化破坏单例;
  • 如何理解 Spring Bean 默认单例和设计模式单例的区别;
  • 如何判断一个对象到底该不该做成单例。

如果只是简单写个 private static final,那确实很快。但单例模式背后的坑,能把“快”变成“快出事”。

这篇博客就系统整理一下单例模式。

正文

chapter 1:什么是单例模式

单例模式,英文是 Singleton Pattern,属于创建型设计模式。

它的核心定义是:

保证一个类只有一个实例,并提供一个全局访问点。

单例模式通常包含三个关键点:

  1. 私有构造方法:防止外部随意 new 对象。
  2. 私有静态实例变量:由类自己保存唯一实例。
  3. 公共静态访问方法:向外提供获取实例的入口。

最典型的结构如下:

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {

private static final Singleton INSTANCE = new Singleton();

private Singleton() {
}

public static Singleton getInstance() {
return INSTANCE;
}
}

这个结构看起来很简单,但不同实现方式在性能、线程安全、懒加载、安全性上有明显差别。

chapter 2:为什么需要单例模式

有些对象在系统中天然就不应该创建多个实例。

例如:

  • 全局配置管理器;
  • 日志管理器;
  • 连接池管理器;
  • 线程池管理器;
  • 缓存管理器;
  • ID 生成器;
  • 应用上下文对象;
  • 监控指标注册中心;
  • 工具类中的状态管理对象。

如果这些对象被重复创建,可能会带来问题:

  • 浪费内存;
  • 状态不一致;
  • 重复初始化资源;
  • 多个实例之间互相打架;
  • 外部系统连接被重复创建;
  • 难以统一管理生命周期。

比如一个 ID 生成器,如果系统里创建了多个实例,而且每个实例都从同一个初始值开始生成 ID,那么就可能出现重复 ID。

这种 bug 不会大喊“我来了”,它通常是半夜线上报警时悄悄出现。挺有礼貌,但很致命。

chapter 3:单例模式的适用场景

单例模式适合以下场景。

1. 系统中只需要一个实例

例如全局配置、全局缓存、全局注册中心。

2. 创建对象成本较高

例如对象初始化时需要加载大量配置、建立连接、读取文件。

3. 需要统一管理共享资源

例如线程池、连接池、限流器、指标收集器。

4. 需要全局访问点

例如某个组件需要在多个地方被访问,而且这个组件本身有统一的状态。

但是要注意:全局访问点不是让你到处乱调的通行证

单例用不好,很容易变成“全局变量换皮肤”。

chapter 4:单例模式的优点

1. 节省资源

全局只创建一个实例,可以减少对象创建和内存开销。

2. 保证状态一致

所有调用方访问的是同一个对象,避免多个实例之间状态不一致。

3. 统一管理生命周期

对象由类自身控制创建过程和访问方式,便于统一初始化和销毁。

4. 提供全局访问点

在需要共享组件的场景下,调用方式比较方便。

chapter 5:单例模式的缺点

1. 可能隐藏依赖关系

如果代码到处使用:

1
ConfigManager.getInstance()

那么类之间的依赖关系会变得不明显,不利于测试和维护。

2. 不利于单元测试

单例对象是全局共享的,如果里面有状态,测试之间可能互相污染。

3. 可能违反单一职责原则

有些单例类既负责创建自己,又负责业务逻辑,还负责全局状态管理,职责容易变重。

4. 多线程场景容易写错

懒加载单例如果处理不当,会在并发场景下创建多个实例。

5. 可能被反射和序列化破坏

普通单例实现无法天然抵抗反射和反序列化。

chapter 6:实现方式一:饿汉式单例

饿汉式单例是最简单、最直接的实现方式。

类加载时就创建实例。

1
2
3
4
5
6
7
8
9
10
11
public class EagerSingleton {

private static final EagerSingleton INSTANCE = new EagerSingleton();

private EagerSingleton() {
}

public static EagerSingleton getInstance() {
return INSTANCE;
}
}

特点

  • 线程安全;
  • 实现简单;
  • 没有加锁开销;
  • 类加载时就初始化;
  • 不支持懒加载。

适用场景

适合实例一定会被使用,而且创建成本不高的场景。

例如:

  • 简单配置对象;
  • 无状态工具管理器;
  • 启动时必须初始化的组件。

缺点

如果这个实例一直没有被使用,也会在类加载时提前创建,可能造成资源浪费。

chapter 7:实现方式二:静态代码块饿汉式

如果对象创建过程需要异常处理,可以使用静态代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StaticBlockSingleton {

private static final StaticBlockSingleton INSTANCE;

static {
try {
INSTANCE = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("Failed to create singleton instance", e);
}
}

private StaticBlockSingleton() {
}

public static StaticBlockSingleton getInstance() {
return INSTANCE;
}
}

特点

  • 线程安全;
  • 可以处理初始化异常;
  • 仍然是类加载时创建;
  • 不支持懒加载。

适用场景

适合初始化过程稍微复杂,但仍然希望启动阶段完成创建的场景。

例如读取本地配置文件、初始化固定规则等。

chapter 8:实现方式三:懒汉式单例,线程不安全

懒汉式单例的核心思想是:

第一次使用时才创建对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LazyUnsafeSingleton {

private static LazyUnsafeSingleton instance;

private LazyUnsafeSingleton() {
}

public static LazyUnsafeSingleton getInstance() {
if (instance == null) {
instance = new LazyUnsafeSingleton();
}

return instance;
}
}

特点

  • 支持懒加载;
  • 实现简单;
  • 线程不安全。

为什么线程不安全

在多线程环境下,可能出现这种情况:

1
2
3
4
线程 A 判断 instance == null,准备创建对象
线程 B 判断 instance == null,也准备创建对象
线程 A 创建一个实例
线程 B 又创建一个实例

最终系统中可能出现多个实例。

是否推荐

不推荐在多线程环境中使用。

除非你能确定这个类只会在单线程环境下使用,否则不要这么写。

chapter 9:实现方式四:同步方法懒汉式

为了保证线程安全,可以给 getInstance() 方法加 synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LazySynchronizedSingleton {

private static LazySynchronizedSingleton instance;

private LazySynchronizedSingleton() {
}

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

return instance;
}
}

特点

  • 支持懒加载;
  • 线程安全;
  • 实现简单;
  • 性能较差。

为什么性能较差

每次调用 getInstance() 都要获取锁。

但是事实上,只有第一次创建对象时需要加锁,实例创建完成后,后续访问不需要加锁。

所以这种写法虽然安全,但性能不够好。

适用场景

适合访问频率不高,而且想简单保证线程安全的场景。

chapter 10:实现方式五:同步代码块懒汉式,错误示范

有人可能会想:既然同步方法锁太大,那我只锁创建对象这一段。

于是写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazyBlockSingleton {

private static LazyBlockSingleton instance;

private LazyBlockSingleton() {
}

public static LazyBlockSingleton getInstance() {
if (instance == null) {
synchronized (LazyBlockSingleton.class) {
instance = new LazyBlockSingleton();
}
}

return instance;
}
}

这段代码看起来减少了锁范围,但它仍然是线程不安全的。

问题在哪里

多线程下可能发生:

1
2
3
4
5
线程 A 判断 instance == null,进入 if
线程 B 判断 instance == null,也进入 if
线程 A 获得锁,创建实例
线程 A 释放锁
线程 B 获得锁,又创建实例

因为进入同步代码块之后没有再次判断 instance == null,所以仍然可能创建多个实例。

这就是一个典型的“看起来优化了,实际上埋雷了”的写法。

chapter 11:实现方式六:双重检查锁,DCL

双重检查锁,英文是 Double-Checked Locking,简称 DCL。

这是比较常见的懒加载单例写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DoubleCheckedLockingSingleton {

private static volatile DoubleCheckedLockingSingleton instance;

private DoubleCheckedLockingSingleton() {
}

public static DoubleCheckedLockingSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLockingSingleton.class) {
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}

return instance;
}
}

为什么要检查两次

第一次检查:

1
if (instance == null)

是为了避免实例创建后,每次调用都进入同步代码块。

第二次检查:

1
if (instance == null)

是为了避免多个线程排队进入锁后重复创建实例。

为什么必须加 volatile

这一点非常重要。

1
private static volatile DoubleCheckedLockingSingleton instance;

对象创建并不是一个完全原子的过程,大致可以拆成三步:

1
2
3
1. 分配内存空间
2. 初始化对象
3. 将引用指向内存地址

如果没有 volatile,可能发生指令重排:

1
2
3
1. 分配内存空间
2. 将引用指向内存地址
3. 初始化对象

这时其他线程可能看到 instance != null,但拿到的是一个还没有初始化完成的对象。

加上 volatile 可以禁止相关指令重排,并保证可见性。

特点

  • 支持懒加载;
  • 线程安全;
  • 性能较好;
  • 实现比饿汉式复杂;
  • 必须使用 volatile

是否推荐

在需要懒加载,并且不想使用静态内部类时,可以使用。

但在现代 Java 中,静态内部类和枚举单例往往更推荐。

chapter 12:实现方式七:静态内部类单例

静态内部类是非常推荐的一种单例实现方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HolderSingleton {

private HolderSingleton() {
}

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

public static HolderSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

为什么它是懒加载

外部类 HolderSingleton 被加载时,内部类 SingletonHolder 不会立即加载。

只有调用:

1
HolderSingleton.getInstance()

并访问 SingletonHolder.INSTANCE 时,内部类才会被加载,实例才会被创建。

为什么它线程安全

Java 类加载机制保证类初始化过程是线程安全的。

也就是说,SingletonHolder 只会被初始化一次。

特点

  • 支持懒加载;
  • 线程安全;
  • 没有显式加锁;
  • 实现优雅;
  • 推荐使用。

适用场景

适合大多数普通单例对象。

例如:

  • 配置读取器;
  • ID 生成器;
  • 本地缓存管理器;
  • 无需依赖外部框架托管的工具组件。

chapter 13:实现方式八:枚举单例

枚举单例是 Joshua Bloch 在 Effective Java 中推荐的一种方式。

1
2
3
4
5
6
7
8
public enum EnumSingleton {

INSTANCE;

public void doSomething() {
System.out.println("枚举单例执行方法");
}
}

使用方式:

1
2
3
4
5
6
public class Client {

public static void main(String[] args) {
EnumSingleton.INSTANCE.doSomething();
}
}

特点

  • 实现最简单;
  • 天然线程安全;
  • 天然防止反射破坏;
  • 天然防止反序列化破坏;
  • 不支持懒加载到方法级别;
  • 可读性对部分团队来说不如普通类直观。

为什么枚举能防止反射破坏

Java 对枚举类型有特殊处理,反射不能像普通类那样创建枚举实例。

如果强行通过反射创建枚举对象,会抛出异常。

为什么枚举能防止反序列化破坏

枚举的序列化机制由 JVM 保证,反序列化时不会创建新的枚举实例。

是否推荐

如果你需要一个非常稳定、简单、安全的单例,枚举单例是很好的选择。

不过在 Spring 项目中,如果对象需要注入依赖、读取配置、参与生命周期管理,通常还是交给 Spring 容器更自然。

chapter 14:实现方式九:CAS 单例

CAS 单例不是最常见的写法,但可以作为理解并发控制的补充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.atomic.AtomicReference;

public class CasSingleton {

private static final AtomicReference<CasSingleton> INSTANCE = new AtomicReference<>();

private CasSingleton() {
}

public static CasSingleton getInstance() {
while (true) {
CasSingleton current = INSTANCE.get();

if (current != null) {
return current;
}

CasSingleton newInstance = new CasSingleton();

if (INSTANCE.compareAndSet(null, newInstance)) {
return newInstance;
}
}
}
}

特点

  • 支持懒加载;
  • 线程安全;
  • 不使用 synchronized;
  • 可能创建多余对象;
  • 自旋在竞争激烈时可能浪费 CPU。

为什么可能创建多余对象

多个线程同时进入时,可能都创建了 new CasSingleton()

但是只有一个线程能通过 compareAndSet(null, newInstance) 设置成功。

其他线程创建出来的对象会被丢弃,等待 GC 回收。

是否推荐

一般不推荐作为常规单例写法。

它更适合用于理解 CAS 思想,而不是日常业务代码。

chapter 15:不同单例实现方式对比

实现方式 是否线程安全 是否懒加载 实现复杂度 是否推荐
饿汉式 推荐,适合简单场景
静态代码块饿汉式 可用
懒汉式,线程不安全 不推荐
同步方法懒汉式 可用,但性能一般
同步代码块懒汉式 不推荐
双重检查锁 DCL 推荐,但要写对
静态内部类 推荐
枚举单例 强烈推荐,安全性最好
CAS 单例 不推荐常规业务使用

如果没有特殊要求,可以记住两个结论:

普通 Java 类单例,优先考虑静态内部类。

需要极强安全性和简洁性,优先考虑枚举单例。

chapter 16:反射如何破坏单例

普通单例模式通过私有构造方法防止外部创建对象。

但反射可以强行访问私有构造方法。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.reflect.Constructor;

public class ReflectionBreakDemo {

public static void main(String[] args) throws Exception {
HolderSingleton instance1 = HolderSingleton.getInstance();

Constructor<HolderSingleton> constructor = HolderSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);

HolderSingleton instance2 = constructor.newInstance();

System.out.println(instance1 == instance2);
}
}

输出可能是:

1
false

这说明单例被破坏了。

如何防御反射破坏

可以在构造方法中增加判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SafeHolderSingleton {

private static boolean initialized = false;

private SafeHolderSingleton() {
synchronized (SafeHolderSingleton.class) {
if (initialized) {
throw new RuntimeException("Singleton instance already exists");
}

initialized = true;
}
}

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

public static SafeHolderSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

但是这种方式并不完美。

反射仍然可以修改 initialized 字段,或者在第一次调用 getInstance() 之前先通过反射创建对象。

所以如果非常在意反射攻击,枚举单例更稳。

chapter 17:序列化如何破坏单例

如果单例类实现了 Serializable,反序列化可能会创建一个新的对象。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.Serializable;

public class SerializableSingleton implements Serializable {

private static final long serialVersionUID = 1L;

private static final SerializableSingleton INSTANCE = new SerializableSingleton();

private SerializableSingleton() {
}

public static SerializableSingleton getInstance() {
return INSTANCE;
}
}

反序列化时可能得到一个新的实例。

解决方式:readResolve

可以增加 readResolve() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.Serializable;

public class SerializableSingleton implements Serializable {

private static final long serialVersionUID = 1L;

private static final SerializableSingleton INSTANCE = new SerializableSingleton();

private SerializableSingleton() {
}

public static SerializableSingleton getInstance() {
return INSTANCE;
}

private Object readResolve() {
return INSTANCE;
}
}

readResolve() 可以在反序列化时返回已有实例,而不是返回新创建的对象。

更简单的方式

使用枚举单例。

枚举天然支持序列化安全。

chapter 18:克隆如何破坏单例

如果单例类实现了 Cloneable,也可能通过 clone() 创建新对象。

错误示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CloneSingleton implements Cloneable {

private static final CloneSingleton INSTANCE = new CloneSingleton();

private CloneSingleton() {
}

public static CloneSingleton getInstance() {
return INSTANCE;
}

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

这样会破坏单例。

解决方式

直接禁止克隆。

1
2
3
4
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("Singleton can not be cloned");
}

或者更简单:不要让单例类实现 Cloneable

chapter 19:类加载器如何影响单例

单例通常是“一个 JVM 中一个实例”。

但严格来说,单例的范围是:

同一个 ClassLoader 加载的同一个类,只有一个实例。

如果同一个类被不同的类加载器加载,那么可能出现多个单例实例。

这种情况在普通业务项目里不常见,但在以下场景需要注意:

  • Tomcat 多 Web 应用;
  • 插件化系统;
  • OSGi;
  • 自定义 ClassLoader;
  • 热部署框架。

所以单例不是宇宙级唯一,它通常只是某个类加载上下文中的唯一。

别把单例想成“天下归一”,它更像“本班第一”。

chapter 20:Spring 中的单例和设计模式单例有什么区别

Spring 中默认 Bean Scope 是 singleton。

例如:

1
2
3
@Service
public class OrderService {
}

默认情况下,Spring 容器中只会创建一个 OrderService Bean。

但是 Spring 单例和设计模式单例不是完全一回事。

1. 设计模式单例

设计模式单例通常由类自己控制实例创建。

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {

private static final Singleton INSTANCE = new Singleton();

private Singleton() {
}

public static Singleton getInstance() {
return INSTANCE;
}
}

特点是:

  • 构造方法私有;
  • 类自己管理实例;
  • 通过静态方法获取实例;
  • 一个 ClassLoader 范围内通常只有一个实例。

2. Spring 单例

Spring 单例由 Spring 容器管理。

1
2
3
@Service
public class UserService {
}

特点是:

  • 构造方法通常不是私有;
  • 实例由 Spring 容器创建;
  • 通过依赖注入使用;
  • 单例范围是一个 Spring 容器;
  • 可以参与 AOP、生命周期回调、配置注入等。

3. 两者核心区别

对比项 设计模式单例 Spring 单例
实例管理者 类自己 Spring 容器
构造方法 通常 private 通常 public 或默认
获取方式 getInstance() 依赖注入
生命周期 类加载或首次调用 容器管理
作用范围 ClassLoader ApplicationContext
是否方便测试 一般 更方便
是否支持依赖注入 不自然 天然支持

在 Spring Boot 项目中,绝大多数业务组件都应该交给 Spring 管理,而不是自己手写单例。

也就是说:

1
2
3
@Service
public class PaymentService {
}

通常比:

1
2
3
4
5
6
7
8
9
10
11
public class PaymentService {

private static final PaymentService INSTANCE = new PaymentService();

private PaymentService() {
}

public static PaymentService getInstance() {
return INSTANCE;
}
}

更适合真实项目。

chapter 21:案例一:ID 生成器单例

下面写一个简单的全局 ID 生成器。

要求:

  • 全局只有一个实例;
  • 支持线程安全生成 ID;
  • 不依赖 Spring;
  • 使用静态内部类实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.concurrent.atomic.AtomicLong;

public class GlobalIdGenerator {

private final AtomicLong sequence = new AtomicLong(0);

private GlobalIdGenerator() {
}

private static class Holder {
private static final GlobalIdGenerator INSTANCE = new GlobalIdGenerator();
}

public static GlobalIdGenerator getInstance() {
return Holder.INSTANCE;
}

public long nextId() {
return sequence.incrementAndGet();
}
}

使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
public class IdGeneratorDemo {

public static void main(String[] args) {
GlobalIdGenerator generator = GlobalIdGenerator.getInstance();

long id1 = generator.nextId();
long id2 = generator.nextId();

System.out.println(id1);
System.out.println(id2);
}
}

这个案例中,GlobalIdGenerator 很适合做成单例。

因为系统中如果存在多个 ID 生成器实例,就可能导致 ID 分配逻辑不统一。

不过真实分布式系统中,ID 生成器不能只靠本地单例。

如果部署多个服务实例,每个 JVM 都有自己的单例对象,仍然可能出现冲突。

这时通常需要:

  • 雪花算法;
  • 数据库号段模式;
  • Redis 自增;
  • Leaf;
  • 美团 Leaf;
  • 百度 UidGenerator;
  • 分布式 ID 服务。

所以本地单例只能保证单个 JVM 内唯一,不能保证分布式全局唯一。

chapter 22:案例二:配置管理器单例

假设系统启动后需要加载一份本地配置,并在运行期间提供读取能力。

可以使用单例配置管理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConfigManager {

private final Map<String, String> configMap = new ConcurrentHashMap<>();

private ConfigManager() {
loadConfig();
}

private static class Holder {
private static final ConfigManager INSTANCE = new ConfigManager();
}

public static ConfigManager getInstance() {
return Holder.INSTANCE;
}

private void loadConfig() {
configMap.put("app.name", "design-pattern-demo");
configMap.put("app.env", "dev");
configMap.put("thread.pool.size", "16");
}

public String get(String key) {
return configMap.get(key);
}

public String getOrDefault(String key, String defaultValue) {
return configMap.getOrDefault(key, defaultValue);
}
}

调用方式:

1
2
3
4
5
6
7
8
9
10
11
12
public class ConfigDemo {

public static void main(String[] args) {
ConfigManager configManager = ConfigManager.getInstance();

String appName = configManager.get("app.name");
String env = configManager.getOrDefault("app.env", "local");

System.out.println(appName);
System.out.println(env);
}
}

这个案例适合用来理解单例模式。

但在真实 Spring Boot 项目中,配置更推荐使用:

  • application.yml
  • @ConfigurationProperties
  • @Value
  • Nacos
  • Apollo
  • Spring Cloud Config

也就是说,单例思想可以学,但真实项目里不要所有配置都手写一个 ConfigManager

chapter 23:案例三:Spring Boot 中更推荐的写法

在 Spring Boot 项目里,如果要做一个全局 ID 生成器,可以直接交给 Spring 容器。

1
2
3
4
5
6
7
8
9
@Component
public class GlobalIdGenerator {

private final AtomicLong sequence = new AtomicLong(0);

public long nextId() {
return sequence.incrementAndGet();
}
}

业务服务中注入使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class OrderService {

private final GlobalIdGenerator globalIdGenerator;

public OrderService(GlobalIdGenerator globalIdGenerator) {
this.globalIdGenerator = globalIdGenerator;
}

public String createOrder() {
long orderId = globalIdGenerator.nextId();

return "ORDER-" + orderId;
}
}

默认情况下,GlobalIdGenerator 在 Spring 容器中就是单例 Bean。

这种方式的优点是:

  • 依赖关系清晰;
  • 方便测试;
  • 可以使用配置注入;
  • 可以结合 AOP;
  • 生命周期由容器管理;
  • 不需要写 getInstance()

所以对于 Spring 项目来说,建议优先使用 Spring Bean 单例,而不是手写传统单例模式。

chapter 24:单例模式和线程安全

很多人容易误解:

单例对象只有一个,所以它一定线程安全。

这是错误的。

单例只保证实例数量,不保证内部状态线程安全。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CounterSingleton {

private static final CounterSingleton INSTANCE = new CounterSingleton();

private int count = 0;

private CounterSingleton() {
}

public static CounterSingleton getInstance() {
return INSTANCE;
}

public int increment() {
return ++count;
}
}

这个类虽然是单例,但 ++count 不是线程安全的。

多线程环境下可能出现计数错误。

应该改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.atomic.AtomicInteger;

public class CounterSingleton {

private static final CounterSingleton INSTANCE = new CounterSingleton();

private final AtomicInteger count = new AtomicInteger(0);

private CounterSingleton() {
}

public static CounterSingleton getInstance() {
return INSTANCE;
}

public int increment() {
return count.incrementAndGet();
}
}

所以要记住:

单例模式解决的是实例唯一性,不是线程安全的一切问题。

对象内部如果有可变状态,仍然要考虑并发安全。

chapter 25:什么时候不应该使用单例

以下场景不建议使用单例。

1. 对象有用户级状态

比如购物车、用户会话、请求上下文。

这些对象应该跟用户或请求绑定,不应该做成全局单例。

2. 对象需要频繁变化

如果对象内部状态变化复杂,单例会让全局状态难以追踪。

3. 为了调用方便而使用单例

如果只是为了避免传参,就把对象做成单例,这是不好的设计。

这会隐藏依赖关系,让代码越来越难测。

4. 分布式系统中误以为单例是全局唯一

本地单例只在当前 JVM 内有效。

如果服务部署了 10 个实例,就有 10 个单例对象。

分布式唯一性需要依赖分布式协调机制,而不是本地单例。

chapter 26:真实项目中的实践建议

1. 普通 Java 项目优先使用静态内部类

如果你不依赖 Spring,又需要懒加载和线程安全,静态内部类是一个很好的默认选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class XxxManager {

private XxxManager() {
}

private static class Holder {
private static final XxxManager INSTANCE = new XxxManager();
}

public static XxxManager getInstance() {
return Holder.INSTANCE;
}
}

2. 对安全性要求高时使用枚举单例

如果你非常在意反射和序列化破坏,枚举单例更稳。

1
2
3
4
5
6
7
public enum XxxManager {

INSTANCE;

public void doSomething() {
}
}

3. Spring 项目优先交给容器

在 Spring Boot 中,大多数服务类、管理类、客户端类都应该交给 Spring 容器管理。

1
2
3
@Component
public class XxxManager {
}

然后通过构造器注入。

4. 单例内部尽量少放可变状态

如果必须放可变状态,使用线程安全的数据结构或并发控制手段。

例如:

  • AtomicInteger
  • AtomicLong
  • ConcurrentHashMap
  • ReadWriteLock
  • synchronized
  • ReentrantLock

5. 不要把单例写成上帝对象

有些项目里会出现这种类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SystemManager {

public void doUserThing() {
}

public void doOrderThing() {
}

public void doPaymentThing() {
}

public void doLogThing() {
}

public void doCacheThing() {
}
}

这种类不是单例模式的问题,而是职责失控。

单例只能保证“一个对象”,不能拯救“一个对象干所有事”。

chapter 27:完整推荐实现

如果不使用 Spring,可以优先使用静态内部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.concurrent.atomic.AtomicLong;

public class GlobalIdGenerator {

private final AtomicLong sequence = new AtomicLong(0);

private GlobalIdGenerator() {
}

private static class Holder {
private static final GlobalIdGenerator INSTANCE = new GlobalIdGenerator();
}

public static GlobalIdGenerator getInstance() {
return Holder.INSTANCE;
}

public long nextId() {
return sequence.incrementAndGet();
}
}

如果使用 Spring Boot,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.stereotype.Component;

import java.util.concurrent.atomic.AtomicLong;

@Component
public class GlobalIdGenerator {

private final AtomicLong sequence = new AtomicLong(0);

public long nextId() {
return sequence.incrementAndGet();
}
}

业务中使用构造器注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.stereotype.Service;

@Service
public class OrderService {

private final GlobalIdGenerator globalIdGenerator;

public OrderService(GlobalIdGenerator globalIdGenerator) {
this.globalIdGenerator = globalIdGenerator;
}

public String createOrder() {
long id = globalIdGenerator.nextId();

return "ORDER-" + id;
}
}

这才是 Spring 项目里更自然的“单例思维”。

chapter 28:一句话总结

单例模式的本质是:

控制对象创建过程,保证一个类在特定范围内只有一个实例,并提供统一访问入口。

它适合全局共享、创建成本高、需要统一管理的对象。

但是它不是万能药。

使用单例前,应该先问自己几个问题:

  1. 这个对象真的只能有一个吗?
  2. 它有没有用户级、请求级状态?
  3. 它在多线程环境下是否安全?
  4. 它是否应该交给 Spring 容器管理?
  5. 它是否会在分布式环境中被误认为全局唯一?

单例模式写起来很小,但背后的工程判断不小。

真正成熟的写法不是“我会 8 种单例实现”,而是“我知道什么时候不该用单例”。

参考资料

  • Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software.
  • Joshua Bloch. Effective Java.
  • Brian Goetz. Java Concurrency in Practice.
  • Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices.
  • Spring Framework Documentation: Bean Scopes.
  • Oracle Java Documentation: Enum Types.
  • Refactoring Guru: Singleton Pattern.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


设计模式:单例模式
https://allendericdalexander.github.io/2026/04/01/java/design/04singleton-pattern-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议