欢迎你来读这篇博客,这篇博客主要是关于单例模式。
其中包括单例模式的核心思想、适用场景、常见实现方式、线程安全问题、反射与序列化破坏问题,以及 Java 后端开发中的真实案例。
序言
单例模式应该是设计模式里最容易“看懂”,但也最容易“写错”的模式之一。
很多人第一次接触单例模式时,觉得它不过就是:
一个类只能创建一个对象。
这句话没错,但只说对了一半。
在真实 Java 项目中,单例模式真正要考虑的问题包括:
- 如何保证全局只有一个实例;
- 如何控制对象创建时机;
- 如何保证多线程环境下安全;
- 如何避免反射破坏单例;
- 如何避免序列化和反序列化破坏单例;
- 如何理解 Spring Bean 默认单例和设计模式单例的区别;
- 如何判断一个对象到底该不该做成单例。
如果只是简单写个 private static final,那确实很快。但单例模式背后的坑,能把“快”变成“快出事”。
这篇博客就系统整理一下单例模式。
正文
chapter 1:什么是单例模式
单例模式,英文是 Singleton Pattern,属于创建型设计模式。
它的核心定义是:
保证一个类只有一个实例,并提供一个全局访问点。
单例模式通常包含三个关键点:
- 私有构造方法:防止外部随意
new 对象。
- 私有静态实例变量:由类自己保存唯一实例。
- 公共静态访问方法:向外提供获取实例的入口。
最典型的结构如下:
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; } }
|
为什么要检查两次
第一次检查:
是为了避免实例创建后,每次调用都进入同步代码块。
第二次检查:
是为了避免多个线程排队进入锁后重复创建实例。
为什么必须加 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 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:一句话总结
单例模式的本质是:
控制对象创建过程,保证一个类在特定范围内只有一个实例,并提供统一访问入口。
它适合全局共享、创建成本高、需要统一管理的对象。
但是它不是万能药。
使用单例前,应该先问自己几个问题:
- 这个对象真的只能有一个吗?
- 它有没有用户级、请求级状态?
- 它在多线程环境下是否安全?
- 它是否应该交给 Spring 容器管理?
- 它是否会在分布式环境中被误认为全局唯一?
单例模式写起来很小,但背后的工程判断不小。
真正成熟的写法不是“我会 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.
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。