在设计模式中,单例模式(Singleton Pattern)被认为是最基础、最常用的一种创建型模式。尽管很多开发者声称早已熟练掌握,但真正理解其实现方式、适用场景以及各种实现方式之间的差异,并非易事。本文将通过实际例子与深度解析,系统性讲清楚什么是单例模式、为什么需要单例模式以及单例模式的六种主流实现方式。
一、什么是单例模式?
单例模式,顾名思义,就是确保在整个应用生命周期中,一个类只能有一个实例,并提供一个全局访问点。
它的核心目标是:
控制实例数量(通常为1)节省系统资源避免资源冲突提供全局访问
二、为什么要用单例模式?
在实际开发中,以下两类场景最常见地需要使用单例模式:
1. 管理临界资源,避免资源冲突
举个例子:日志文件写入类。
public class Logger {
public void log(String message) {
// 向 log.txt 写入内容
}
}
如果每次都通过 new Logger() 创建对象,在并发环境下,多个 Logger 实例可能同时向同一个文件写入日志,导致文件句柄冲突、数据错乱甚至抛出异常。
通过将 Logger 类设计为单例,可以确保只有一个实例操作该文件,从而有效避免并发写入冲突。
2. 保证全局唯一对象,协调统一管理
典型例子:数据库连接池、主键生成器等。
例如下面这个 ID 生成器类:
public class IdGenerator {
private long counter = 0;
public synchronized long getId() {
return counter++;
}
}
如果每次都创建新的 IdGenerator 实例,每个对象的 counter 初始值都是 0,生成的 ID 就会出现重复,破坏唯一性约束。因此,ID 生成器必须在整个系统中保持单例。
三、单例模式的六种实现方式
实现思路简述:
实现方式是否线程安全是否延迟加载复杂度适用场景饿汉式是否简单对资源需求小或初始化成本低的对象懒汉式否(需加锁)是中对性能要求低的小型系统双重检查是是稍复杂性能优化场景静态内部类是是较优雅推荐实践方式枚举式是是最简单官方推荐,适合所有场景容器式(扩展)视实现而定可支持通常较复杂大量单例管理需求
1. 饿汉式(Eager Initialization)
实现原理:类加载时就完成实例化,线程安全,但不支持延迟加载。
public class IdGenerator {
private static final IdGenerator instance = new IdGenerator();
private IdGenerator() {}
public static IdGenerator getInstance() {
return instance;
}
}
优点:
简单易懂JVM 保证线程安全
缺点:
即使未使用也会创建实例,浪费资源(尤其是重量级类)
2. 懒汉式(Lazy Initialization)
实现原理:在第一次调用时创建对象。非线程安全,需手动加锁。
public class IdGenerator {
private static IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
}
问题:多线程环境下可能创建多个实例,必须加锁。
加锁后的线程安全版本:
public class IdGenerator {
private static IdGenerator instance;
private IdGenerator() {}
public static synchronized IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
}
缺点:
首次调用性能差多线程性能瓶颈明显
3. 双重检查锁(Double-Checked Locking)
实现原理:先判断是否需要加锁,加锁后再次判断是否需要创建实例。
public class IdGenerator {
private static volatile IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) {
synchronized (IdGenerator.class) {
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
}
关键点:
volatile 防止指令重排第一次判断提升性能第二次判断保证线程安全
适用场景:对性能有要求但又需要延迟加载的场景
4. 静态内部类(推荐方式)
实现原理:利用 Java 类加载机制延迟加载,且由 JVM 保证线程安全。
public class IdGenerator {
private IdGenerator() {}
private static class SingletonHolder {
private static final IdGenerator INSTANCE = new IdGenerator();
}
public static IdGenerator getInstance() {
return SingletonHolder.INSTANCE;
}
}
优点:
延迟加载线程安全实现简单优雅
注意:类加载器会在第一次调用 getInstance() 时加载内部类,从而实现懒加载。
5. 枚举方式(官方推荐)
实现原理:利用 Java 枚举的天然单例特性。
public enum IdGenerator {
INSTANCE;
private long counter = 0;
public synchronized long getId() {
return counter++;
}
}
使用方式:
long id = IdGenerator.INSTANCE.getId();
优点:
写法最简单JVM 从语言层面保证线程安全和单例性防止反射与反序列化攻击
缺点:
写法略有语法限制,不适合对类结构有复杂依赖的情境
6. 容器式单例(扩展)
有些系统需要管理多个单例类,可使用容器统一管理。
public class SingletonManager {
private static Map
public static void registerService(String key, Object instance) {
if (!singletonMap.containsKey(key)) {
singletonMap.put(key, instance);
}
}
public static Object getService(String key) {
return singletonMap.get(key);
}
}
适合场景:框架或容器层封装多个单例服务
四、如何选择单例实现?
实现方式延迟加载线程安全推荐级别适用说明饿汉式否是⭐⭐⭐类加载即实例化,适合轻量级单例懒汉式是否(需加锁)⭐代码简单,但线程不安全双重检查是是⭐⭐性能和安全兼顾,复杂度高静态内部类是是⭐⭐⭐⭐推荐实现,优雅安全枚举方式是是⭐⭐⭐⭐⭐最安全简洁,官方推荐容器式可选视情况而定⭐⭐管理多个单例的场景
五、实际项目中如何落地?
示例:日志管理类
public enum Logger {
INSTANCE;
public void log(String message) {
// 线程安全写入日志
}
}
示例:主键生成器
public class IdGenerator {
private long counter = 0;
private IdGenerator() {}
private static class Holder {
private static final IdGenerator INSTANCE = new IdGenerator();
}
public static IdGenerator getInstance() {
return Holder.INSTANCE;
}
public synchronized long getId() {
return counter++;
}
}
六、总结
单例模式的本质是控制对象的唯一性和全局访问性。不同的实现方式适配不同的性能、线程安全、延迟加载等需求。推荐使用:静态内部类 或 枚举方式,优雅、安全、简洁。避免使用懒汉式无同步版本,会引入并发问题。单例并非银弹,合理场景下使用,配合线程安全设计尤为关键。
建议:每当你写出一个 new 的时候,不妨思考下:“这个类是不是该是一个单例?”