设计模式之单例模式
单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一个全局访问点来访问该实例。
一、概念
单例模式(Singleton Pattern)的目标是确保一个类只有一个实例,并提供一个全局访问点。
这意味着无论在程序的任何位置,都应该能够访问到该类的唯一实例。通过将实例的创建和访问逻辑集中管理,可以避免不一致状态,并提高内存使用效率。
二、特点
| 特点 | 说明 |
|---|---|
| 唯一实例 | 单例类只能有一个实例 |
| 自行创建 | 单例类必须自己创建自己的唯一实例 |
| 全局访问 | 单例类必须给所有其他对象提供这一实例 |
💡 核心思想:单例模式的类必须负责创建自己的实例,并在必要时向其他对象提供该实例。
三、代码实现
3.1 直接使用静态类的方式
这种方式在日常编码开发中很常见,它可以在第一次运行时直接初始化 Map 类,同时也不需要等到延迟加载在使用。在不需要维持任何状态情况下,仅仅用于全局访问。
public static Map<String,String> cache = new ConcurrentHashMap<>();3.2 懒汉式单例模式(线程不安全)
⚠️ 注意:这种方式在多线程场景下不满足单例模式的要求,可能会创建多个实例。
public class Singleton_01 {
private static Singleton_01 singleton01;
private Singleton_01(){}
public static Singleton_01 getInstance(){
if(singleton01 == null){
singleton01 = new Singleton_01();
}
return singleton01;
}
}3.3 懒汉式(线程安全)
描述: 这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁
synchronized才能保证单例,但加锁会影响效率。
💡 适用场景:
getInstance()的性能对应用程序不是很关键(该方法使用不太频繁)。
public class Singleton_02 {
private static Singleton_02 singleton02;
private Singleton_02(){}
public static synchronized Singleton_02 getInstance(){
if(singleton02 == null){
singleton02 = new Singleton_02();
}
return singleton02;
}
}3.4 双重校验锁模式
这个模式在简易的懒汉单例模式加上了 volatile 和 synchronized 来保证了创建对象的顺序和保证只能有一个线程来进行创建对象。
singleton 为什么要加上 volatile 关键字?
主要原因就是 singleton = new Singleton(); 不是一个原子操作。
在 JVM 中,这句语句至少做了 3 件事:
- 给 Singleton 的实例分配内存空间;
- 调用
Singleton()的构造函数,初始化成员字段; - 将 singleton 指向分配的内存空间(此时 singleton 就不是 null 了)。
因为存在着指令重排序的优化,第 2、3 步的顺序是不能保证的,最后的执行顺序可能是 1→2→3,也可能是 1→3→2,假如执行顺序是 1→3→2,就可能出现线程拿到未初始化完成的对象。
✅
volatile的作用:禁止指令重排序,保证对象完全初始化后才对其他线程可见。
public class Singleton_05 {
private static volatile Singleton_05 singleton05;
private Singleton_05() {
}
public static Singleton_05 getInstance() {
if (singleton05 != null) {
return singleton05;
}
synchronized (Singleton_05.class) {
if (singleton05 == null) {
// new 不是原子操作,volatile 用于禁止指令重排序,避免返回未初始化完成的对象
singleton05 = new Singleton_05();
}
}
return singleton05;
}
}3.5 饿汉式(线程安全)
描述: 这种方式比较常用,但容易产生垃圾对象。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
public class Singleton_03 {
private static final Singleton_03 singleton03 = new Singleton_03();
private Singleton_03(){
}
public static Singleton_03 getInstance(){
return singleton03;
}
}3.6 使用类的内部类实现单例模式
描述: 这种方式既保证了线程安全,又保证了懒汉模式同时也不会因为加锁而降低性能,对静态域使用延迟初始化。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。这种方式同样利用了 classloader(类加载)机制来保证初始化 instance 时只有一个线程。
它跟饿汉式不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到懒加载效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。
因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。
想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比饿汉式就显得很合理。
✅ 推荐理由:同时满足 线程安全、延迟加载、高性能 三大特性。
public class Singleton_04 {
private static class SingletonHolder{
private static final Singleton_04 singleton04 = new Singleton_04();
}
private Singleton_04(){
}
public static Singleton_04 getInstance(){
return SingletonHolder.singleton04;
}
}四、实现方式对比
| 实现方式 | 对应类 | 线程安全 | 延迟加载 | 性能 | 是否推荐 | 适用场景 |
|---|---|---|---|---|---|---|
| 静态类方式 | — | ✅ | ❌ | 高 | ⚠️ 视情况 | 全局常量、无状态工具类 |
| 懒汉式(不安全) | Singleton_01 | ❌ | ✅ | 高 | ❌ | 单线程环境(不推荐) |
| 懒汉式(安全) | Singleton_02 | ✅ | ✅ | 低 | ❌ | 调用频率极低的场景 |
| 饿汉式 | Singleton_03 | ✅ | ❌ | 高 | ✅ | 实例小、初始化快的场景 |
| 静态内部类 | Singleton_04 | ✅ | ✅ | 高 | ✅✅ 首选 | 绝大多数单例场景 |
| 双重校验锁 DCL | Singleton_05 | ✅ | ✅ | 高 | ✅✅ | 需要延迟加载 + 可控性强的场景 |
五、总结与推荐
最佳实践建议
优先选择「静态内部类」方式(Singleton_04):无需额外锁机制,实现简洁,同时满足线程安全、懒加载、高性能三大核心需求。
若实例占用资源极小:直接使用「饿汉式」(Singleton_03)即可,简单高效,JVM 层面保证线程安全。
若需要更精细的控制(如实例化过程复杂、需要传递参数等):选择「双重校验锁 DCL」(Singleton_05),务必不要忘记
volatile关键字。永远不要使用:线程不安全的懒汉式(Singleton_01)和加锁粒度太大的
synchronized懒汉式(Singleton_02)。
防破坏提示
即使实现了上述单例,仍有三种方式可能破坏单例,需要额外注意:
- 反射攻击:通过反射调用私有构造器 → 可在构造器中加判断,第二次实例化时抛出异常
- 序列化破坏:序列化后再反序列化会生成新对象 → 需实现
readResolve()方法返回已有实例 - 克隆破坏:实现
Cloneable接口后调用clone()→ 重写clone()方法,直接返回已有实例