目录
[3.1 线程不安全版(错误示范)](#3.1 线程不安全版(错误示范))
[3.2 synchronized 同步方法版](#3.2 synchronized 同步方法版)
[3.3 双重检查锁 DCL(最常用)](#3.3 双重检查锁 DCL(最常用))
[6.1 反射破坏](#6.1 反射破坏)
[6.2 序列化破坏](#6.2 序列化破坏)
[七、Spring 中的单例](#七、Spring 中的单例)
引言
在开发中经常会有这样一类对象:整个应用只需要一个,多了反而会出问题。比如数据库连接池、线程池、配置管理类、日志对象、Spring 容器里的 Bean......如果这些对象被反复 new 出来,不仅浪费资源,还可能引发状态不一致、端口占用等一连串问题。
单例模式(Singleton Pattern)就是用来解决"保证一个类只有一个实例,并提供一个全局访问点"这个问题的。它属于创建型设计模式,也是面试和日常开发里最常打交道的模式之一。这篇文章我会从最简单的写法一路讲到生产环境最推荐的写法,并带你看看单例是怎么被"破坏"、又该怎么防范的。
一、什么是单例模式
单例模式的核心就两点:
- 私有化构造方法 :让外部无法通过
new随意创建实例。 - 提供一个静态的全局访问方法:通过这个方法获取唯一实例。
一个最朴素的单例骨架长这样:
java
java
public class Singleton {
// 1. 私有构造方法
private Singleton() {}
// 2. 静态方法返回唯一实例
public static Singleton getInstance() {
// 返回唯一实例
return ...;
}
}
关键在于"唯一实例"到底怎么创建、什么时候创建、怎么保证线程安全,这正是下面要展开的。
二、饿汉式(最简单)
饿汉式的意思是"一上来就创建好",不管用不用,类加载时就 new 出实例。
java
java
public class Singleton {
// 类加载时就创建实例,线程安全
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
- 优点:实现最简单;基于类加载机制天然线程安全,没有加锁开销。
- 缺点:如果这个实例很"重"(占用大量资源)而你又一直没用它,就造成了浪费。
- 适用场景:实例创建开销小、或确定一定会用到的场景。
三、懒汉式(用时才创建)
懒汉式的意思是"懒",第一次用到的时候才去创建实例。
3.1 线程不安全版(错误示范)
java
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 多线程下可能创建多个实例!
}
return instance;
}
}
问题 :如果两个线程同时进入 if (instance == null) 判断,就会各自 new 一个实例,破坏单例。所以这个写法在多线程环境下是错误的。
3.2 synchronized 同步方法版
最简单的补救是给方法加锁:
java
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 优点:线程安全了。
- 缺点 :每次调用
getInstance()都要加锁,即使实例早就创建好了,性能较差。
3.3 双重检查锁 DCL(最常用)
为了"既线程安全、又只在第一次创建时加锁",出现了双重检查锁(Double-Checked Locking):
java
java
public class Singleton {
// volatile 保证可见性、禁止指令重排
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关键字 :new Singleton()这条语句在底层不是原子的,会发生"分配内存 → 初始化对象 → 将引用指向内存"三步,其中后两步可能被指令重排。如果没有volatile,另一个线程可能拿到一个"还没初始化完成"的对象。加了volatile可以禁止这种重排,保证内存可见性。
四、静态内部类(优雅且推荐)
利用 JVM 类加载机制实现懒加载 + 线程安全,代码更简洁:
java
java
public class Singleton {
private Singleton() {}
// 静态内部类,只有在被使用时才会加载
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
- 懒加载 :
Holder内部类只有在getInstance()第一次被调用时才会被 JVM 加载,此时才创建实例。 - 线程安全 :由 JVM 的类加载机制保证,无需任何锁和
volatile。 - 优点:写法简洁、无锁、性能好,是除了枚举之外最推荐的写法之一。
五、枚举实现(最推荐)
《Effective Java》作者 Joshua Bloch 推荐的终极写法:
java
java
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("单例方法被调用");
}
}
// 使用
Singleton.INSTANCE.doSomething();
- 线程安全:枚举由 JVM 保证只初始化一次。
- 防反射、防序列化:枚举天然免疫后面要讲的两种破坏方式,是最安全的单例实现。
- 唯一"缺点":如果你的单例需要继承某个类,枚举就不适用了(枚举不能继承)。除此之外,能上枚举就上枚举。
六、破坏单例的两种方式与防范
6.1 反射破坏
上面的饿汉式、懒汉式、静态内部类都靠"私有构造方法"来阻止外部 new,但反射可以绕过这个限制:
java
java
public static void main(String[] args) throws Exception {
Singleton s1 = Singleton.getInstance();
// 通过反射拿到私有构造方法
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 暴力破解私有权限
Singleton s2 = constructor.newInstance();
System.out.println(s1 == s2); // false!单例被破坏
}
防范:在构造方法里加判断,如果实例已存在则抛异常:
java
java
private Singleton() {
if (instance != null) {
throw new RuntimeException("单例已存在,禁止反射创建");
}
}
不过最省心的还是直接用枚举 ,反射也拿它没办法(Constructor.newInstance 对枚举会直接抛异常)。
6.2 序列化破坏
如果一个单例类实现了 Serializable 接口,那么"序列化再反序列化"也会得到一个新对象:
java
java
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
// 序列化 → 反序列化后,得到的是新实例
防范 :加一个 readResolve() 方法,反序列化时返回已有的唯一实例:
java
java
// 反序列化时会被调用,返回单例本身
private Object readResolve() {
return INSTANCE;
}
七、Spring 中的单例
如果你用 Spring,那"单例"几乎无处不在。Spring 容器管理的 Bean,**默认作用域就是单例(singleton)**:
java
java
@Service // 默认就是单例
public class UserService {
// ...
}
同一个 Bean 在整个容器里只有一个实例,被所有注入它的地方共享。这也是为什么我们在 Service 里不要定义有状态的可变成员变量(比如用成员变量存请求相关的数据),否则多线程并发访问同一单例 Bean 时会产生数据串扰问题。
java
java
@Service
public class UserService {
// ❌ 不要这样:多线程共享同一个 Bean,会串数据
private String currentUserName;
public void doSomething(String name) {
this.currentUserName = name; // 并发下会被其他线程覆盖
}
}
总结
文章我们梳理了单例模式的完整知识:
- 饿汉式:最简单、天然线程安全,但可能浪费资源。
- 懒汉式 :用时才创建,但要注意线程安全------普通写法不安全,
synchronized方法性能差,双重检查锁 DCL (配合volatile)最常用。 - 静态内部类:借助 JVM 类加载机制实现懒加载 + 线程安全,优雅无锁。
- 枚举:最安全、最推荐,天然防反射、防序列化,能上就上。
- 破坏与防范 :反射和序列化都能破坏单例,可通过构造方法守卫和
readResolve()防范,枚举天然免疫。 - Spring:容器中的 Bean 默认就是单例,因此要避免在单例 Bean 里持有可变状态。
一句话总结:日常开发优先用枚举;不能枚举就用静态内部类或双重检查锁;同时牢记单例"无状态"的使用原则。
顺带提一下接下来六篇我应该都会更新设计模式相关的内容,之后我会好好打磨一个博客项目给大家分享OVO