目录
[3.双重检查锁 DCL](#3.双重检查锁 DCL)
[1. 反射破坏](#1. 反射破坏)
[2. 序列化破坏](#2. 序列化破坏)
一、前言
单例模式几乎是 Java 面试里的**"常驻嘉宾"**。很多人会背几种写法,但一旦面试官继续追问"为什么线程不安全""为什么要加 volatile""静态内部类为什么可行",就容易卡住。
这篇文章不只讲"怎么写",更想讲清楚"为什么这么写"。我们从最基础的实现开始,一步步手撕到线程安全、性能优化,以及反射和序列化这些高频追问。
二、什么是单例模式
单例模式,顾名思义,就是一个类在整个系统中只允许存在一个实例,并且提供一个全局访问入口。
它的核心目标只有两个:
- 保证唯一实例
- 提供统一访问方式
常见场景包括:
- 配置中心
- 日志组件
- 数据库连接管理器
- 缓存管理器
- 线程池管理器
听起来很简单,但真正难的地方在于:既要保证只有一个对象,又要在并发环境下安全,还希望性能别太差。
一个标准的单例,通常要满足:
- 构造器私有化,防止外部 new
- 类内部自己持有唯一实例
- 对外提供获取实例的方法
最基本的结构如下:
java
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
三、实现单例模式多种版本
1.懒汉式写法
懒汉式,最容易写,也最容易出问题,思路是:对象先不创建,等第一次用到时再创建。
java
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点
- 延迟加载
- 写法直观
缺点
- 线程不安全
为什么线程不安全?
假设线程 A 和线程 B 同时进入 getInstance(),并且都判断 instance == null 成立,那么它们都会执行 new Singleton(),最终就可能创建出多个对象。
所以,这一版只能在单线程环境下用,面试里如果只写到这里,基本一定会被追问。
也可以在getInstance()上加锁防止线程安全问题
2.饿汉式写法
饿汉式,简单粗暴,天然线程安全,思路和懒汉式相反:类加载时就直接把实例创建好。
java
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
优点
- 实现简单
- 天然线程安全
- 调用性能好
为什么线程安全?
因为类加载过程本身就是线程安全的,JVM 会保证类只加载一次,所以静态实例也只会初始化一次。
缺点
- 没有延迟加载
- 如果这个对象一直没被用到,就会造成一定资源浪费
适用场景
如果这个单例对象本身很轻量,或者项目启动后大概率一定会用到,那饿汉式完全没问题。
3.双重检查锁 DCL
为了兼顾线程安全和性能,很多人会想到:既然每次都加锁太重,那能不能只在第一次创建时加锁?
这就是双重检查锁。
java
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;
}
}
为什么要判空两次
第一次 if (instance == null):
- 避免每次都进入同步块
- 提高性能
第二次 if (instance == null):
- 防止多个线程进入第一层判断后,重复创建对象
为什么 volatile 不能少
这部分是面试最爱问的点。
new Singleton() 这行代码看起来像一个原子操作,但实际上底层大致会经历三步:
- 分配内存
- 初始化对象
- 将引用赋值给 instance
问题在于,JVM 可能发生指令重排,变成:
- 分配内存
- 将引用赋值给 instance
- 初始化对象
如果线程 A 执行到第 2 步,此时线程 B 进来发现 instance != null,就直接返回了一个"还没初始化完成"的对象,这就会出问题。
volatile 的作用就是:
- 禁止指令重排
- 保证可见性
所以,DCL 必须搭配 volatile 使用,否则就是不完整写法。
4.静态内部类
很多时候,工程里更推荐静态内部类写法。
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() 时,才会触发 Holder 的加载和初始化。
而类加载过程又是线程安全的,因此它天然保证了:
- 延迟加载
- 线程安全
- 不需要显式加锁
优点
- 写法简洁
- 性能好
- 延迟加载
- 线程安全
很多场景下,这一版是比 DCL 更推荐的选择。
四、单例模式会被怎么破坏
很多人以为把构造器私有化就万无一失了,其实不够。
1. 反射破坏
即使构造器是 private,也可以通过反射强行访问。
java
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s1 = constructor.newInstance();
Singleton s2 = constructor.newInstance();
System.out.println(s1 == s2); // false
如何防御
可以在构造器里增加判断:
java
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
if (instance != null) {
throw new RuntimeException("Singleton already exists");
}
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2. 序列化破坏
如果单例类实现了 Serializable,序列化再反序列化后,可能得到一个新对象。
java
Singleton s1 = Singleton.getInstance();
// 序列化 s1
// 反序列化得到 s2
System.out.println(s1 == s2); // 可能是 false
如何防御
实现 readResolve():
java
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
private Object readResolve() {
return INSTANCE;
}
}
这样反序列化时,返回的仍然是原来的单例对象。
五、单例模式总结
单例模式表面上只是"让一个类只能创建一个对象",但真正的难点在于并发安全和边界问题。
我们可以把它理解成三个层次:
- 会写单例
- 写对线程安全的单例
- 理解为什么这样写,以及它还会被什么方式破坏
如果只让我给一个工程里比较推荐的版本,我会优先选静态内部类:
java
public class Singleton {
private Singleton() {
}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
单例模式真正要掌握的,不是背下哪几段代码,而是明白:
- 为什么懒汉式会出并发问题
- 为什么 DCL 要配 volatile
- 为什么类加载机制能保证线程安全
- 为什么反射和序列化可能破坏单例
制作不易,如果对你有帮助请**点赞,评论,收藏,**感谢大家的支持
