单例模式是 23种设计模式中最简单、面试最高频、生产最常用 的创建型模式。几乎所有框架都大量使用单例:Spring Bean、线程池、配置工具类、缓存管理器、日志工具、连接池等。
很多人只会写饿汉、懒汉,但不懂:哪种写法线程安全?哪种有性能问题?哪种能防反射?哪种能防序列化破坏?生产到底用哪种?
本文一次性讲透单例模式核心思想、八种实现、优劣对比、底层隐患、安全加固、真实应用场景,看完彻底吃透单例所有考点。
一、什么是单例模式?核心定义
单例模式(Singleton) :保证一个类在整个JVM中仅有一个实例对象,并提供一个全局访问入口。
单例核心三大特征:
-
私有构造方法:禁止外部 new 对象
-
私有静态实例:类内部唯一实例
-
公有静态获取方法:对外统一获取实例入口
二、为什么需要单例?解决什么问题
2.1 核心优势
-
节省内存:全局仅一个对象,避免频繁创建销毁对象,减少GC压力
-
节省资源开销:连接池、线程池、配置类创建成本极高,必须全局唯一
-
统一全局状态:全局配置、全局计数器、全局缓存必须保证唯一实例
-
方便全局访问:无需传参,随处可获取实例
2.2 使用场景总结
只要是 工具类、配置类、资源池、全局管理器,都适合单例:
-
Spring 默认单例 Bean
-
数据库连接池、线程池
-
日志工具、脱敏工具、常量工具类
-
全局配置信息、系统参数管理
-
状态管理器、缓存管理器
三、单例八种写法(全网最全)+ 优劣+线程安全分析
重点:只有 3 种是生产可用:饿汉式、双重检查锁DCL、枚举单例,其余全部存在线程安全问题或性能问题。
1. 饿汉式(静态常量)------【线程安全、推荐简单场景】
原理:类加载阶段直接初始化实例,天然避免多线程竞争。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
优点:简单、线程绝对安全、无锁、性能高
缺点 :类加载即创建,存在内存浪费(不用也创建)
适用:小对象、必定使用的全局工具类
2. 饿汉式(静态代码块)
和上面完全一致,只是写法不同,同样类加载初始化。
3. 普通懒汉式 ---【线程不安全,禁止生产使用】
用到才创建,实现懒加载,但多线程下会创建多个实例。
问题:多线程同时判断 null,会穿透创建多个对象。
4. 同步方法懒汉式 ---【线程安全、性能极差】
方法加 synchronized,保证安全,但 每次获取实例都加锁,高并发严重卡顿。
结论:绝不生产使用。
5. 双重检查锁 DCL(经典生产写法)------【高并发最优】
业界标准写法、面试核心:兼顾懒加载、线程安全、高性能。
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 对象 底层三步:开辟内存 → 初始化对象 → 赋值引用
CPU 指令重排可能变成:开辟内存 → 赋值引用 → 初始化对象
导致其他线程获取到 半初始化脏对象,引发空指针、数据异常。
volatile 作用:禁止指令重排、保证可见性,彻底杜绝半初始化问题。
优点:懒加载、高并发无锁、性能极高
缺点:无法防止反射、序列化破坏
6. 静态内部类写法 ---【极简优雅、线程安全】
public class Singleton {
private Singleton(){}
// 静态内部类:外部类加载不会加载内部类
private static class Inner{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return Inner.INSTANCE;
}
}
原理:利用静态内部类延迟加载特性,天然线程安全。
优点:极简、懒加载、无锁、高性能
缺点:依然无法防反射、序列化破坏
7. 枚举单例 ---【最强安全、面试天花板】
public enum SingletonEnum {
INSTANCE;
}
《Effective Java》官方推荐最优单例
唯一优点:
-
天然线程安全
-
天然防止反射破坏
-
天然防止序列化破坏
-
写法极简、无漏洞
唯一缺点:不支持懒加载(类加载即初始化)
生产结论 :能使用枚举优先枚举,安全等级最高。
四、单例两大致命漏洞:反射破坏 + 序列化破坏
这是面试 进阶必问:除枚举外,所有自定义单例都能被破解。
4.1 反射破坏单例
通过反射获取私有构造器、强行 new 多个对象,打破单例唯一性。
唯独枚举不能被反射破坏:JDK 底层禁止反射创建枚举实例,直接抛异常。
4.2 序列化破坏单例
对象序列化写入磁盘、再反序列化读取,会生成全新对象,破坏单例。
修复方案 :重写 readResolve() 方法,返回全局唯一实例。
枚举天然杜绝:序列化机制底层保证永远返回唯一枚举常量。
五、四种常用单例最终取舍(生产规范)
-
安全优先、无需懒加载 → 枚举单例(最优)
-
高并发、需要懒加载 → DCL 双重检查锁
-
简单工具类、无需懒加载 → 饿汉式
-
极简优雅、业务普通场景 → 静态内部类
六、Spring 单例与手写单例的区别
-
手写单例:JVM 级别单例,全局永远唯一
-
Spring 单例 :容器级别单例,一个 IoC 容器一个实例,多容器依然多实例
Spring Bean 默认单例底层是通过 一级缓存 singletonObjects 实现单例复用。
七、单例模式优缺点总结
优点
-
减少对象创建、节省内存、降低GC压力
-
全局统一实例,状态统一、配置统一
-
减少资源开销(连接池、线程池优势极大)
-
全局访问便捷
缺点
-
全局强耦合:过度使用导致代码依赖混乱
-
无接口、不利于扩展(传统写法)
-
并发漏洞多:写法不当极易线程不安全
-
不利于单元测试:全局硬编码单例难以Mock
八、全文终极面试总结
1. 核心作用:保证类全局唯一实例,节省资源、统一状态、减少对象创建开销。
2. 线程安全写法:饿汉、静态内部类、DCL、枚举。
3. DCL volatile作用:禁止指令重排,防止获取半初始化脏对象。
4. 最强单例:枚举单例,天然防反射、防序列化、线程安全。
5. 漏洞根源:普通单例可被反射、序列化破坏;枚举JDK底层保护无解漏洞。
6. 生产选型:安全选枚举、并发懒加载选DCL、简单工具类选饿汉。