文章目录
-
- 前言
- [1. 单例模式是什么?](#1. 单例模式是什么?)
- [2. 为什么要用单例?能解决什么问题?](#2. 为什么要用单例?能解决什么问题?)
- [3. 单例模式的实现思路](#3. 单例模式的实现思路)
- [4. 饿汉式单例(线程安全,创建早)](#4. 饿汉式单例(线程安全,创建早))
-
- [4.1 代码示例(核心思想:类加载即初始化)](#4.1 代码示例(核心思想:类加载即初始化))
- [4.2 优缺点](#4.2 优缺点)
- [5. 懒汉式单例(延迟创建,但线程不安全)](#5. 懒汉式单例(延迟创建,但线程不安全))
-
- [5.1 代码示例(核心思想:第一次调用才创建)](#5.1 代码示例(核心思想:第一次调用才创建))
- [5.2 为什么线程不安全?](#5.2 为什么线程不安全?)
- [6. 懒汉式 + synchronized(线程安全,但性能差)](#6. 懒汉式 + synchronized(线程安全,但性能差))
-
- [6.1 代码示例(方法级锁)](#6.1 代码示例(方法级锁))
- [6.2 优缺点](#6.2 优缺点)
- [7. 双重检查锁(Double-Checked Locking, DCL)](#7. 双重检查锁(Double-Checked Locking, DCL))
-
- [7.1 代码示例(核心思想:减少同步开销)](#7.1 代码示例(核心思想:减少同步开销))
- [7.2 为什么要 `volatile`?](#7.2 为什么要
volatile?) - [7.3 优缺点](#7.3 优缺点)
- [8. 静态内部类单例(懒加载 + 线程安全)](#8. 静态内部类单例(懒加载 + 线程安全))
-
- [8.1 代码示例](#8.1 代码示例)
- [8.2 为什么它线程安全?](#8.2 为什么它线程安全?)
- [8.3 优缺点](#8.3 优缺点)
- [9. 枚举单例(防止"反射/序列化攻击"的方式)](#9. 枚举单例(防止“反射/序列化攻击”的方式))
-
- [9.1 代码示例](#9.1 代码示例)
- [9.2 优点](#9.2 优点)
- [9.3 缺点](#9.3 缺点)
- [10. 单例模式的注意事项](#10. 单例模式的注意事项)
- [11. 单例模式的适用场景总结](#11. 单例模式的适用场景总结)
- [12. 一句话总结:怎么选实现方式?](#12. 一句话总结:怎么选实现方式?)
前言
在软件开发中,"某个类只应该有一个实例,并且提供全局访问点"是非常常见的需求。单例模式就是为了解决这个问题:保证对象唯一 ,并且统一入口 访问该对象。

1. 单例模式是什么?
单例模式 :确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
常见用途:
- 配置管理类(读取配置、全局生效)
- 线程池/缓存管理器
- 日志器(Logger)
- 数据库连接池(通常已有更专业实现,但思路类似)
2. 为什么要用单例?能解决什么问题?
单例模式通常用于以下场景:
- 控制资源数量:比如某些昂贵资源(线程池、缓存、硬件设备等)不应该重复创建。
- 共享状态/统一管理:需要全局统一的管理入口。
- 避免重复初始化:系统启动时初始化一次即可,后续复用。
3. 单例模式的实现思路
从实现方式看,常见单例分为三大类:
- 饿汉式(类加载时就创建)
- 懒汉式(第一次使用才创建)
- 线程安全懒汉式(如:双重检查锁 DCL、静态内部类等)
此外还有:
- 枚举单例(Java 中最推荐之一的写法,天然防反序列化破坏)
- 注册式/容器式单例(更灵活,但不属于经典 GoF 单例)
4. 饿汉式单例(线程安全,创建早)
4.1 代码示例(核心思想:类加载即初始化)
java
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
4.2 优缺点
优点
- 天然线程安全:JVM 类加载机制保证初始化过程是安全的
- 实现简单
缺点
- 如果程序运行中从未用到这个单例,会造成不必要的资源占用
- 可能导致类加载变慢(在某些场景)
适用场景
- 单例创建成本不高
- 或者几乎确定程序会用到该单例
5. 懒汉式单例(延迟创建,但线程不安全)
5.1 代码示例(核心思想:第一次调用才创建)
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
5.2 为什么线程不安全?
在多线程环境下可能出现:
- 线程 A 判断
instance == null,准备创建 - 线程 B 也判断
instance == null,也创建 - 最终产生多个实例
因此:懒汉式要想正确,就必须处理并发。
6. 懒汉式 + synchronized(线程安全,但性能差)
6.1 代码示例(方法级锁)
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
6.2 优缺点
优点
- 线程安全,逻辑清晰
缺点
- 每次调用
getInstance()都要获得锁,性能较差 - 高并发下锁竞争明显
适用场景
- 并发不高、对性能要求不高的场景(但一般不推荐)
7. 双重检查锁(Double-Checked Locking, DCL)
7.1 代码示例(核心思想:减少同步开销)
java
public class Singleton {
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;
}
}
7.2 为什么要 volatile?
关键点在于指令重排问题:
- 没加
volatile时,可能发生这样的顺序:- 分配内存
- 将内存地址赋给
instance - 调用构造函数初始化对象
此时另一个线程可能看到 instance != null,但对象还没初始化完成。
volatile 能禁止关键重排,并保证可见性。
详细了解volatile相关可以看我的另一篇文章 volatile的三大特性、底层原理
7.3 优缺点
优点
- 懒加载
- 高并发下性能比 synchronized 方案好
缺点
- 代码更复杂
- 容易漏掉
volatile
适用场景
- 需要懒加载 + 高并发性能
8. 静态内部类单例(懒加载 + 线程安全)
8.1 代码示例
java
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
8.2 为什么它线程安全?
Holder类只有在第一次调用getInstance()时才会被加载- JVM 类加载过程是线程安全的
- 因此能保证
INSTANCE只初始化一次
8.3 优缺点
优点
- 懒加载
- 线程安全
- 代码简洁,不需要 synchronized / volatile
缺点
- 初学者可能不理解"静态内部类触发加载"的机制
适用场景
- 通常工程里这是非常推荐的实现方式
9. 枚举单例(防止"反射/序列化攻击"的方式)
9.1 代码示例
java
public enum Singleton {
INSTANCE;
public void someMethod() {}
}
9.2 优点
- 天然支持线程安全
- 防止反射创建新实例(enum 的机制更可靠)
- 防止反序列化破坏单例(JVM 语义保证)
9.3 缺点
- 不能继承其他类(enum 本身限制)
- 用法风格不同于常规类
适用场景
- 用于"必须绝对唯一"的单例场景
10. 单例模式的注意事项
-
反射破坏单例
- 普通单例(尤其懒汉式/饿汉式)可能被反射绕过
- 枚举单例更安全
-
序列化破坏单例
- 如果实现了
Serializable,需要考虑readResolve()保证反序列化返回同一个实例 - enum 单例可避免很多问题
- 如果实现了
-
多线程可见性与指令重排(DCL)
- DCL 必须加
volatile(这是最常见错误)
- DCL 必须加
-
单例不是越多越好
- 单例会带来全局状态,滥用可能引发可维护性、测试困难、隐式耦合
-
是否真的需要单例?
- 很多场景其实是"全局可访问的对象",但不一定要严格"唯一实例"
- 例如依赖注入(DI)容器常常能更优雅地解决"单实例需求"
11. 单例模式的适用场景总结
适合:
- 需要全局唯一实例的场景
- 资源初始化成本高且只需要一次
- 工具类/管理类(配置、日志、缓存等)
不适合:
- 业务上强依赖可变全局状态(容易造成耦合)
- 需要频繁创建销毁、区分上下文的对象(这种不应该用单例)
12. 一句话总结:怎么选实现方式?
- 想简单:饿汉式
- 想懒加载又要简洁:静态内部类
- 想懒加载 + 高并发 + 熟悉细节:双重检查锁(DCL)+ volatile
- 强安全要求(反射/序列化):枚举单例