1. 单例模式基础认知
第一次接触单例模式是在2013年参与电商平台开发时,当时需要全局维护一个商品库存校验服务。团队里的架构师在代码评审时指着我的实例化代码说:"这里得用单例,不然每次请求都new对象,内存要爆炸的"。这句话让我牢牢记住了单例模式的核心价值——控制实例数量,节约系统资源。
单例模式(Singleton Pattern)作为创建型设计模式中最简单的一种,其定义却非常明确:确保一个类只有一个实例,并提供一个全局访问点。在实际工程中,这种模式的应用场景非常广泛:
- 配置信息管理(如数据库连接参数)
- 日志记录器(避免重复创建文件句柄)
- 线程池管理
- 缓存系统实现
- 设备驱动对象(如打印机服务)
在Java中实现单例模式时,有两个经典流派:饿汉式(Eager Initialization)和懒汉式(Lazy Initialization)。它们最本质的区别在于初始化时机——前者在类加载时就急不可耐地创建实例,后者则等到第一次被调用时才懒洋洋地实例化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 饿汉式实现解析
2.1 经典实现代码
让我们先看一个标准的饿汉式实现:
java复制public class EagerSingleton {
// 在类加载时就完成初始化
private static final EagerSingleton instance = new EagerSingleton();
// 私有化构造器
private EagerSingleton() {}
// 全局访问点
public static EagerSingleton getInstance() {
return instance;
}
}
2.2 初始化时机分析
饿汉式的核心特点就在那个final关键字上。JVM在加载EagerSingleton类时,会立即执行静态变量的初始化,此时instance成员就已经被赋值。这种机制利用了JVM的类加载特性:
- 当首次访问EagerSingleton类时(可能是调用getInstance或访问其他静态成员)
- JVM加载并初始化该类
- 执行静态变量instance的初始化(new操作)
- 后续所有对getInstance的调用都直接返回已创建的实例
2.3 线程安全性证明
饿汉式天然具备线程安全性的关键在于类加载过程的线程安全保证。JVM规范明确要求类初始化阶段必须同步处理,这相当于内置了一个隐式的同步锁。我们可以通过以下测试代码验证:
java复制public class SingletonConcurrencyTest {
public static void main(String[] args) {
final int THREAD_COUNT = 100;
final Set<EagerSingleton> instances = Collections.synchronizedSet(new HashSet<>());
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executor.execute(() -> {
instances.add(EagerSingleton.getInstance());
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("产生的实例数量:" + instances.size());
// 输出始终为1
}
}
2.4 优缺点对比
优势:
- 实现简单直观,代码可读性高
- 无任何同步开销,性能最佳
- 被final修饰的实例可防止被反射修改
劣势:
- 类加载时就初始化,可能造成资源浪费(如果实例一直未被使用)
- 无法传递参数进行初始化(因为构造时机不可控)
- 大量饿汉式单例会导致应用启动变慢
实际工程建议:适合那些初始化耗时短、必定会被使用的核心服务。我在支付网关开发中就常用饿汉式来管理支付渠道配置。
3. 懒汉式实现演进
3.1 基础版(非线程安全)
最原始的懒汉式实现是这样的:
java复制public class NaiveLazySingleton {
private static NaiveLazySingleton instance;
private NaiveLazySingleton() {}
public static NaiveLazySingleton getInstance() {
if (instance == null) {
instance = new NaiveLazySingleton();
}
return instance;
}
}
这个版本在多线程环境下会创建多个实例,完全违背了单例的初衷。我曾在预发环境遇到过因此导致的缓存穿透问题——三个实例同时操作Redis导致数据不一致。
3.2 同步方法版
最简单的改进方案是给整个方法加锁:
java复制public synchronized static SyncLazySingleton getInstance() {
if (instance == null) {
instance = new SyncLazySingleton();
}
return instance;
}
这种实现虽然保证了线程安全,但每次调用都要获取锁,性能损失严重。压测数据显示QPS下降达90%,完全不适合高并发场景。
3.3 双重检查锁定(DCL)
经过多次迭代,业界最终形成了DCL(Double-Checked Locking)方案:
java复制public class DCLSingleton {
private volatile static DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (DCLSingleton.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new DCLSingleton(); // 初始化
}
}
}
return instance;
}
}
这里有几个关键点:
volatile关键字防止指令重排序(避免返回未初始化完成的对象)- 外层检查避免不必要的同步
- 内层检查防止重复创建
踩坑记录:曾经忘记加volatile导致线上出现罕见的NPE,原因是JVM的指令重排序可能使得instance引用先于构造函数执行完毕。
3.4 静态内部类方案
更优雅的解决方案是利用静态内部类:
java复制public class HolderSingleton {
private HolderSingleton() {}
private static class Holder {
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
return Holder.INSTANCE;
}
}
这种实现兼具了懒加载和线程安全的优点:
- 只有调用getInstance时才会加载Holder类
- 类加载机制保证初始化线程安全
- 无需同步开销
4. 深度对比与选型指南
4.1 特性对比表
| 维度 | 饿汉式 | 懒汉式(DCL) | 静态内部类 |
|---|---|---|---|
| 初始化时机 | 类加载时 | 首次调用时 | 首次调用时 |
| 线程安全 | 天然安全 | 需双重检查 | 天然安全 |
| 性能 | 无锁,最佳 | 第一次有同步开销 | 无锁,最佳 |
| 实现复杂度 | 最简单 | 较复杂 | 中等 |
| 防反射破坏 | 可防(final) | 需额外处理 | 可防(final) |
| 序列化安全 | 需readResolve | 需readResolve | 需readResolve |
4.2 典型应用场景
选择饿汉式当:
- 实例初始化耗时极短(<1ms)
- 实例必定会被使用(如核心配置)
- 对启动性能不敏感的系统
选择懒汉式当:
- 实例初始化耗时长(如连接池)
- 可能存在永不使用的场景
- 需要运行时传递初始化参数
特别推荐静态内部类方案:
- 适合绝大多数常规场景
- 平衡了实现复杂度和性能
- 在Spring等框架中被广泛采用
4.3 面试常考点
在技术面试中,单例模式的考察通常会深入到以下层面:
- 指令重排序与volatile的关系(DCL为什么需要volatile)
- 类加载机制如何保证线程安全
- 反射攻击的防范措施(如枚举实现)
- 序列化破坏单例的解决方案
- 不同实现方案的内存可见性问题
我曾用下面这段代码成功防御了反射攻击:
java复制public class AntiReflectSingleton {
private static boolean initialized = false;
private AntiReflectSingleton() {
synchronized (AntiReflectSingleton.class) {
if (initialized) {
throw new RuntimeException("禁止反射攻击");
}
initialized = true;
}
}
// 其余实现同DCL...
}
5. 现代Java中的演进
5.1 枚举实现方案
Joshua Bloch在《Effective Java》中推荐的枚举实现:
java复制public enum EnumSingleton {
INSTANCE;
public void businessMethod() {
// 业务逻辑
}
}
这种实现:
- 绝对防止多实例(包括反射和序列化)
- 代码极其简洁
- 天然支持枚举特性
缺点是不支持懒加载,且继承体系受限。
5.2 JDK中的实践案例
Java标准库中有许多单例实践:
Runtime.getRuntime():饿汉式Desktop.getDesktop():同步懒汉式Collections.EMPTY_LIST:静态常量实现
特别值得注意的是System.console()方法,它采用了延迟初始化但不保证只创建唯一实例,这提醒我们:不是所有看起来像单例的API都是真正的单例。
5.3 与依赖注入框架的配合
在Spring环境下,单例模式有了新的变化:
java复制@Service // 默认就是单例
public class OrderService {
// 依赖注入的单例Bean
@Autowired
private PaymentService paymentService;
}
Spring的单例与经典实现不同之处在于:
- 作用域是IoC容器级别(非JVM级别)
- 通过BeanFactory管理生命周期
- 支持AOP代理
在微服务架构下,真正的全局单例需要考虑分布式环境,此时通常需要借助Redis或ZooKeeper等中间件实现。
