单例模式是Java设计模式中最基础也最常用的创建型模式,核心目标是保证一个类在程序运行期间只有一个实例,并提供全局访问点。本文将详细拆解三种经典单例实现方式(饿汉式、懒汉式、双重校验锁),结合代码示例分析各自的优缺点与适用场景。
一、单例模式的核心设计原则
在实现单例前,需先明确三个核心约束:
- 私有化构造器:禁止外部通过
new关键字创建实例; - 私有静态成员变量:存储类的唯一实例;
- 公共静态方法:提供全局访问入口,返回唯一实例。
所有单例实现都围绕这三个原则展开,区别仅在于实例创建时机和线程安全处理方式。
二、饿汉式单例(饿汉模式)
1. 核心特征
- 创建时机:类加载时(ClassLoader加载类阶段)就完成实例化,属于“预加载”;
- 线程安全:天然线程安全(类加载由JVM保证线程安全,只会执行一次);
- 优点:实现简单、无并发问题;
- 缺点:可能造成资源浪费(实例创建后未被使用,也会占用内存)。
2. 经典代码实现
饿汉式有两种常见写法:静态变量版和静态代码块版,本质逻辑一致。
写法1:静态变量直接初始化
/**
* 饿汉式单例 - 静态变量版
* 类加载时直接创建实例,线程安全但可能浪费资源
*/
public class HungrySingleton {
// 1. 私有静态成员变量:类加载时初始化,全局唯一
private static final HungrySingleton INSTANCE = new HungrySingleton();
// 2. 私有化构造器:禁止外部new
private HungrySingleton() {
// 防止反射破坏单例(可选增强)
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
}
// 3. 公共静态方法:提供全局访问入口
public static HungrySingleton getInstance() {
return INSTANCE;
}
// 测试方法
public void sayHello() {
System.out.println("饿汉式单例:Hello Singleton!");
}
}
写法2:静态代码块初始化(适合复杂初始化逻辑)
/**
* 饿汉式单例 - 静态代码块版
* 适合需要加载配置文件、初始化资源等复杂场景
*/
public class HungrySingletonWithStaticBlock {
// 1. 私有静态成员变量(先声明,后初始化)
private static final HungrySingletonWithStaticBlock INSTANCE;
// 2. 静态代码块:类加载时执行,完成实例初始化
static {
try {
// 模拟复杂初始化逻辑(如读取配置文件)
INSTANCE = new HungrySingletonWithStaticBlock();
} catch (Exception e) {
throw new RuntimeException("单例初始化失败", e);
}
}
// 3. 私有化构造器
private HungrySingletonWithStaticBlock() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
}
// 4. 公共静态方法
public static HungrySingletonWithStaticBlock getInstance() {
return INSTANCE;
}
public void sayHello() {
System.out.println("饿汉式单例(静态代码块):Hello Singleton!");
}
}
3. 测试代码
public class SingletonTest {
public static void main(String[] args) {
// 饿汉式测试
HungrySingleton instance1 = HungrySingleton.getInstance();
HungrySingleton instance2 = HungrySingleton.getInstance();
System.out.println(instance1 == instance2); // true(同一实例)
instance1.sayHello();
}
}
三、懒汉式单例(懒汉模式)
1. 核心特征
- 创建时机:第一次调用
getInstance()方法时才实例化,属于“懒加载”; - 线程安全:基础版非线程安全,需加锁保证安全;
- 优点:延迟加载,避免资源浪费;
- 缺点:加锁后会降低并发性能。
2. 经典代码实现
写法1:基础版(非线程安全,禁止生产使用)
/**
* 懒汉式单例 - 基础版(非线程安全)
* 仅适合单线程环境,多线程下会创建多个实例
*/
public class LazySingletonUnsafe {
// 1. 私有静态成员变量(不初始化)
private static LazySingletonUnsafe INSTANCE;
// 2. 私有化构造器
private LazySingletonUnsafe() {}
// 3. 公共静态方法:第一次调用时创建实例
public static LazySingletonUnsafe getInstance() {
if (INSTANCE == null) { // 判空:未创建则初始化
INSTANCE = new LazySingletonUnsafe();
}
return INSTANCE;
}
}
写法2:加锁版(线程安全,性能较低)
/**
* 懒汉式单例 - 加锁版(线程安全)
* 用synchronized保证线程安全,但每次调用都加锁,性能差
*/
public class LazySingletonSafe {
private static LazySingletonSafe INSTANCE;
private LazySingletonSafe() {}
// 方法上加synchronized,保证同一时间只有一个线程执行
public static synchronized LazySingletonSafe getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySingletonSafe();
}
return INSTANCE;
}
public void sayHello() {
System.out.println("懒汉式单例(加锁版):Hello Singleton!");
}
}
3. 问题分析
- 基础版在多线程下,多个线程同时进入
if (INSTANCE == null)会创建多个实例,破坏单例; - 加锁版虽然线程安全,但
synchronized加在方法上,每次调用getInstance()都会加锁,即使实例已创建,仍会有锁竞争,性能损耗大。
四、双重校验锁(DCL)单例
1. 核心特征
- 创建时机:懒加载(第一次调用时创建);
- 线程安全:双重判空 + 局部锁,兼顾线程安全与性能;
- 优点:懒加载 + 线程安全 + 高性能(仅实例创建时加锁);
- 注意点:需给实例变量加
volatile关键字,防止指令重排。
2. 经典代码实现
/**
* 双重校验锁(DCL)单例 - 懒加载 + 线程安全 + 高性能
* 生产环境最常用的单例实现方式
*/
public class DclSingleton {
// 1. 私有静态成员变量:加volatile防止指令重排
private static volatile DclSingleton INSTANCE;
// 2. 私有化构造器
private DclSingleton() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
}
// 3. 公共静态方法:双重判空 + 局部锁
public static DclSingleton getInstance() {
// 第一次判空:实例已创建时,直接返回,无需加锁(提升性能)
if (INSTANCE == null) {
// 局部锁:仅实例未创建时加锁,减少锁竞争
synchronized (DclSingleton.class) {
// 第二次判空:防止多个线程等待锁后重复创建实例
if (INSTANCE == null) {
INSTANCE = new DclSingleton();
}
}
}
return INSTANCE;
}
public void sayHello() {
System.out.println("DCL单例:Hello Singleton!");
}
}
3. 关键知识点:volatile的作用
INSTANCE = new DclSingleton() 并非原子操作,JVM会拆分为3步:
- 分配内存空间;
- 初始化实例对象;
- 将INSTANCE指向分配的内存地址。
若无 volatile,JVM可能发生指令重排(步骤2和3交换),导致线程A创建实例时,线程B看到 INSTANCE != null,但实例未完成初始化,进而获取到“半初始化”的实例,引发空指针异常。
volatile 关键字会禁止指令重排,保证实例创建的完整性。
4. 测试代码
public class SingletonTest {
public static void main(String[] args) {
// DCL单例测试(多线程环境)
for (int i = 0; i < 10; i++) {
new Thread(() -> {
DclSingleton instance = DclSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + instance);
}).start();
}
// 输出结果:所有线程获取的是同一个实例
}
}
五、三种单例模式对比与选型建议
| 实现方式 | 线程安全 | 懒加载 | 性能 | 适用场景 |
|---|---|---|---|---|
| 饿汉式 | 是 | 否 | 高 | 实例占用资源少、启动即使用 |
| 懒汉式(加锁) | 是 | 是 | 低 | 单线程环境、对性能要求低 |
| 双重校验锁(DCL) | 是 | 是 | 高 | 多线程环境、高性能要求(推荐) |
选型核心原则:
- 简单场景:优先饿汉式(实现简单,无并发问题);
- 资源敏感场景:优先DCL(懒加载+高性能);
- 禁止使用:懒汉式基础版(非线程安全);
- 特殊场景:若需防止反射/序列化破坏单例,可使用枚举单例(《Effective Java》推荐,本文暂不展开)。
六、总结
单例模式的核心是“唯一实例 + 全局访问”,三种经典实现各有侧重:
- 饿汉式胜在简单、线程安全,但牺牲了懒加载;
- 懒汉式实现了懒加载,但基础版不安全,加锁版性能差;
- 双重校验锁(DCL)是平衡“懒加载、线程安全、高性能”的最优解,也是生产环境中最常用的单例写法。