文章目录
-
- [1. 引言:从实际问题出发](#1. 引言:从实际问题出发)
- [2. 单例模式的核心思想](#2. 单例模式的核心思想)
- [3. 实现方式大全](#3. 实现方式大全)
-
- [3.1 饿汉式(两种变体)](#3.1 饿汉式(两种变体))
-
- [3.1.1 静态变量饿汉式](#3.1.1 静态变量饿汉式)
- [3.1.2 静态代码块饿汉式](#3.1.2 静态代码块饿汉式)
- [3.2 懒汉式(四种演进)](#3.2 懒汉式(四种演进))
-
- [3.2.1 最基础的懒汉式(线程不安全)](#3.2.1 最基础的懒汉式(线程不安全))
- [3.2.2 方法同步懒汉式(线程安全,但性能差)](#3.2.2 方法同步懒汉式(线程安全,但性能差))
- [3.2.3 双重检查锁(DCL)](#3.2.3 双重检查锁(DCL))
- [3.2.4 静态内部类(推荐)](#3.2.4 静态内部类(推荐))
- [3.3 枚举式(最佳实践)](#3.3 枚举式(最佳实践))
- [4. 防御性编程](#4. 防御性编程)
-
- [4.1 防止反射攻击](#4.1 防止反射攻击)
- [4.2 防止序列化破坏](#4.2 防止序列化破坏)
- [5. 实战应用](#5. 实战应用)
- [6. 总结对比表](#6. 总结对比表)

1. 引言:从实际问题出发
在日常开发中,我们经常会遇到这样的需求:
- 全局只需要一个配置中心
- 数据库连接池只需要一个实例
- 日志系统不希望被重复初始化
- 线程池、缓存管理器需要统一入口
如果这些对象被创建多次,可能会带来:
- 资源浪费(连接、内存)
- 状态不一致(多个配置实例)
- 并发问题(多个线程操作不同实例)
这类问题的核心诉求只有一句话:
某个类在整个应用生命周期中,只允许存在一个实例,并且能被全局访问。
这正是单例模式(Singleton Pattern)要解决的问题。
2. 单例模式的核心思想
单例模式的定义非常简单:
保证一个类只有一个实例,并提供一个访问它的全局访问点。
从设计角度看,单例模式包含三个关键点:
- 构造方法私有化
- 防止外部通过
new创建对象
- 防止外部通过
- 类内部持有唯一实例
- 对外提供静态访问方法
抽象成一句话就是:
"自己创建自己,并且只创建一次。"
3. 实现方式大全
单例模式的实现方式很多,本质区别在于两个维度:
- 实例创建时机:是否提前创建
- 线程安全保障方式
单例设计模式分类两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
下面将逐一展开介绍。
3.1 饿汉式(两种变体)
3.1.1 静态变量饿汉式
java
public class Singleton {
//在成员位置创建该类的对象
private static final Singleton INSTANCE = new Singleton();
//私有构造方法
private Singleton() {}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return INSTANCE;
}
}
特点:
- 类加载时即创建实例
- 线程安全(JVM 类加载机制保证)
- 实现简单
缺点:
- 不管是否使用,实例都会被创建
- 如果实例创建成本高,可能造成资源浪费
说明:该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
适用场景:
- 对象轻量
- 启动即需要使用
3.1.2 静态代码块饿汉式
java
public class Singleton {
private static final Singleton INSTANCE;
static {
INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
与静态变量方式的区别:
- 可以在静态代码块中加入异常处理或复杂逻辑
- 本质仍然是类加载即初始化
也存在内存浪费问题
3.2 懒汉式(四种演进)
懒汉式的核心思想是:
不到真正需要时,不创建实例。
但这也直接引入了线程安全问题。
3.2.1 最基础的懒汉式(线程不安全)
java
public class Singleton {
//在成员位置创建该类的对象
private static Singleton instance;
//私有构造方法
private Singleton() {}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
问题:
- 多线程下可能创建多个实例
- 生产环境绝对不能用
这是一个"教学用反例"。从上面代码我们可以看出该方式在成员位置声明
Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
3.2.2 方法同步懒汉式(线程安全,但性能差)
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:
- 线程安全
缺点:
- 每次调用都加锁
- 性能开销大
该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
3.2.3 双重检查锁(DCL)
java
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,如果instance不为null,不进入抢锁阶段,直接返回实例
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查,抢到锁之后再次判断是否为null
instance = new Singleton();
}
}
}
return instance;
}
}
关键点:
volatile防止指令重排序- 双重判断减少锁竞争
这是懒加载 + 高性能 + 线程安全的经典实现
添加
volatile关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
3.2.4 静态内部类(推荐)
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
java
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
原理:
- 外部类加载时不会初始化内部类
- 调用
getInstance()时才触发内部类加载 - JVM 保证类加载线程安全
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance(),虚拟机加载Holder
并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
优点:
- 延迟加载
- 无需显式同步
- 代码优雅
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
3.3 枚举式(最佳实践)
java
public enum Singleton {
INSTANCE;
public void doSomething() {
// ...
}
}
为什么这是最佳实践?
- JVM 保证枚举只会被实例化一次
- 天然防止:
- 反射攻击
- 反序列化破坏
- 写法极简
《Effective Java》明确推荐:单例首选枚举实现
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
4. 防御性编程
即使实现了单例,也可能被"破坏"。
4.1 防止反射攻击
java
private static boolean initialized = false;
private Singleton() {
if (initialized) {
throw new RuntimeException("禁止反射创建实例");
}
initialized = true;
}
但请注意:
枚举单例天生免疫反射攻击。
4.2 防止序列化破坏
java
private Object readResolve() {
return INSTANCE;
}
否则:
- 反序列化会生成新对象
- 单例被破坏
同样,枚举不需要任何处理。
5. 实战应用
5.1 JDK 源码赏析
Runtime
java
public class Runtime {
private static final Runtime currentRuntime = new Runtime();
private Runtime() {}
public static Runtime getRuntime() {
return currentRuntime;
}
}
典型的饿汉式单例。
5.2 何时使用 / 避免使用
适合使用:
- 配置管理器
- 线程池
- 缓存
- 日志系统
谨慎使用:
- 有大量状态的对象
- 需要频繁替换实现的场景
- 单元测试(单例会增加测试难度)
滥用单例 = 隐式全局变量
6. 总结对比表
| 实现方式 | 是否懒加载 | 线程安全 | 推荐程度 |
|---|---|---|---|
| 饿汉式 | 否 | 是 | ⭐⭐⭐ |
| 懒汉式(基础) | 是 | 否 | ❌ |
| 同步懒汉式 | 是 | 是 | ⭐⭐ |
| 双重检查锁 | 是 | 是 | ⭐⭐⭐⭐ |
| 静态内部类 | 是 | 是 | ⭐⭐⭐⭐⭐ |
| 枚举式 | 否 | 是 | ⭐⭐⭐⭐⭐⭐ |
参考资料