单例模式的核心原则
实现单例的核心要求:
- 私有构造方法(防止外部通过
new创建实例); - 类内部创建唯一实例;
- 提供公共静态方法获取该实例;
- 保证多线程环境下实例唯一(线程安全)。
写法 1:饿汉式(静态常量)—— 最简单的线程安全单例
代码实现
/** * 饿汉式(静态常量) * 特点:线程安全、无懒加载、类加载时就初始化实例 */ public class Singleton1 { // 1. 私有构造方法,禁止外部new private Singleton1() {} // 2. 类加载时就创建唯一实例(饿汉:提前初始化) private static final Singleton1 INSTANCE = new Singleton1(); // 3. 提供公共方法获取实例 public static Singleton1 getInstance() { return INSTANCE; } }解析
- 线程安全:类加载过程由 JVM 保证线程安全(静态变量初始化在类加载的
<clinit>方法中,JVM 会保证该方法被加锁,多线程下只会执行一次); - 优点:实现简单、无线程安全问题、性能高;
- 缺点:无懒加载(类加载时就初始化实例),如果实例创建成本高(如占内存大、依赖外部资源),且程序全程未使用该实例,会造成资源浪费;
- 适用场景:实例创建成本低、程序启动后大概率会使用的单例。
写法 2:饿汉式(静态代码块)—— 初始化逻辑复杂的饿汉式
代码实现
/** * 饿汉式(静态代码块) * 特点:线程安全、无懒加载,适合初始化逻辑复杂的场景 */ public class Singleton2 { private Singleton2() {} // 先声明实例,静态代码块中初始化(适合需要读取配置文件等复杂逻辑) private static Singleton2 INSTANCE; static { // 可添加复杂初始化逻辑,如读取配置、连接数据库等 INSTANCE = new Singleton2(); } public static Singleton2 getInstance() { return INSTANCE; } }解析
- 核心逻辑和「静态常量饿汉式」一致,只是把实例初始化放到静态代码块中;
- 适用场景:单例初始化需要执行复杂逻辑(如读取配置文件、初始化资源)时。
写法 3:懒汉式(线程不安全)—— 笔试常考 “反面教材”
代码实现
/** * 懒汉式(线程不安全) * 特点:懒加载(使用时才初始化),但多线程下会创建多个实例,实际开发禁止使用 */ public class Singleton3 { private Singleton3() {} private static Singleton3 INSTANCE; // 调用方法时才初始化(懒加载) public static Singleton3 getInstance() { if (INSTANCE == null) { // 多线程下此处会并发执行,创建多个实例 INSTANCE = new Singleton3(); } return INSTANCE; } }解析
- 线程不安全:多线程同时调用
getInstance()时,会同时进入if (INSTANCE == null),创建多个实例,破坏单例; - 优点:懒加载,节省资源;
- 缺点:线程不安全,仅适用于单线程环境;
- 笔试考点:面试官常问 “这个写法有什么问题?”,需答出 “多线程下会创建多个实例”。
写法 4:懒汉式(线程安全,同步方法)—— 安全但低效
代码实现
/** * 懒汉式(同步方法) * 特点:线程安全、懒加载,但每次调用getInstance都加锁,效率低 */ public class Singleton4 { private Singleton4() {} private static Singleton4 INSTANCE; // 加synchronized保证线程安全,但每次调用都锁整个方法,效率低 public static synchronized Singleton4 getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton4(); } return INSTANCE; } }解析
- 线程安全:
synchronized修饰静态方法,保证多线程下只有一个线程能进入方法; - 优点:懒加载、线程安全;
- 缺点:效率极低 —— 即使实例已创建,后续所有调用
getInstance()的线程都要排队获取锁,浪费资源; - 适用场景:单例调用频率极低的场景(几乎不用)。
写法 5:双重校验锁(DCL)—— 实际开发最常用
代码实现
/** * 双重校验锁(DCL,Double Check Lock) * 特点:线程安全、懒加载、效率高,实际开发首选 */ public class Singleton5 { private Singleton5() {} // 关键:volatile修饰,防止指令重排序 private static volatile Singleton5 INSTANCE; public static Singleton5 getInstance() { // 第一次校验:实例已创建则直接返回,无需加锁(提高效率) if (INSTANCE == null) { synchronized (Singleton5.class) { // 锁类对象,保证只有一个线程进入 // 第二次校验:防止多个线程等待锁后重复创建实例 if (INSTANCE == null) { INSTANCE = new Singleton5(); } } } return INSTANCE; } }核心解析(笔试高频考点)
为什么需要两次校验?
- 第一次校验(外层
if):实例已创建时,直接返回,避免进入同步块,提高效率; - 第二次校验(内层
if):假设两个线程同时通过外层if,等待锁后,只有一个线程能进入同步块,第二个线程进入时,实例已创建,避免重复初始化。
- 第一次校验(外层
为什么需要 volatile?
INSTANCE = new Singleton5()不是原子操作,JVM 会拆分为 3 步:① 分配内存空间;② 初始化实例;③ 把 INSTANCE 指向分配的内存地址。- 若无 volatile,JVM 可能发生指令重排序(①→③→②),导致:线程 A 执行完①③,INSTANCE 已非 null,但实例未初始化;线程 B 进入外层
if,发现 INSTANCE≠null,直接返回未初始化的实例,引发空指针异常。 - volatile 禁止指令重排序,保证①→②→③的执行顺序,避免上述问题。
优缺点
- 优点:线程安全、懒加载、效率高(仅初始化时加锁,后续无锁);
- 缺点:实现稍复杂,需注意 volatile 不能省略;
- 适用场景:绝大多数实际开发场景(如工具类、连接池单例)。
写法 6:静态内部类 —— 优雅的懒加载单例
代码实现
/** * 静态内部类单例 * 特点:线程安全、懒加载、实现优雅,《Effective Java》推荐写法之一 */ public class Singleton6 { private Singleton6() {} // 静态内部类:不会随外部类加载而初始化,只有调用getInstance时才加载 private static class SingletonHolder { // JVM保证静态常量初始化的线程安全 private static final Singleton6 INSTANCE = new Singleton6(); } public static Singleton6 getInstance() { // 调用此方法时,才加载SingletonHolder,创建INSTANCE return SingletonHolder.INSTANCE; } }解析
- 懒加载:外部类
Singleton6加载时,内部类SingletonHolder不会加载;只有调用getInstance()时,内部类才加载,创建实例; - 线程安全:JVM 保证静态内部类的
<clinit>方法线程安全,实例只会创建一次; - 优缺点:
- 优点:线程安全、懒加载、实现优雅(无需手动加锁 /volatile);
- 缺点:无法防止反射破坏单例(所有非枚举单例都有此问题)。
写法 7:枚举单例 —— 最安全的单例(《Effective Java》推荐)
代码实现
/** * 枚举单例 * 特点:天然线程安全、防止反射/序列化破坏单例,最简单的完美单例 */ public enum Singleton7 { // 唯一实例(枚举的每个常量都是单例) INSTANCE; // 可添加单例的业务方法 public void doSomething() { System.out.println("枚举单例执行方法"); } } // 使用方式 // Singleton7.INSTANCE.doSomething();核心优势(笔试必答)
- 天然线程安全:枚举类的初始化由 JVM 保证,多线程下只会创建一个实例;
- 防止反射破坏:反射无法创建枚举实例(
Constructor.newInstance()会抛异常); - 防止序列化破坏:枚举的序列化由 JVM 处理,反序列化不会创建新实例;
- 实现最简单:无需手动处理锁、volatile 等,代码极简。
优缺点
- 优点:绝对线程安全、防反射 / 序列化、实现简单;
- 缺点:无懒加载(枚举类加载时就初始化实例);
- 适用场景:需要绝对安全的单例场景(如配置中心、核心工具类)。
总结
| 写法 | 线程安全 | 懒加载 | 防反射 / 序列化 | 适用场景 |
|---|---|---|---|---|
| 饿汉式(静态常量) | ✅ | ❌ | ❌ | 实例创建成本低、必用的单例 |
| 懒汉式(同步方法) | ✅ | ✅ | ❌ | 调用频率极低的单例(几乎不用) |
| 双重校验锁(DCL) | ✅ | ✅ | ❌ | 绝大多数实际开发场景 |
| 静态内部类 | ✅ | ✅ | ❌ | 追求优雅实现的懒加载单例 |
| 枚举单例 | ✅ | ❌ | ✅ | 要求绝对安全的单例 |
关键点回顾
- 笔试高频考点:DCL 写法中 volatile 的作用、双重校验的原因、枚举单例的优势;
- 实际开发优先选:DCL(需懒加载)、枚举单例(需绝对安全)、静态内部类(追求优雅);
- 所有非枚举单例的构造方法私有,仍可通过反射暴力破解(设置
setAccessible(true)),枚举单例可彻底避免。