设计模式——单例模式(饿汉式,懒汉式等)
目录
- 设计模式——单例模式(饿汉式,懒汉式等)
- 概念
- 核心要点
- 实现
- 基础要点
- 饿汉式
- 懒汉式
- 懒汉式(线程安全,双重检查锁定)
- 静态内部类实现
- 使用枚举实现
- 总结
概念
单例模式(Singleton Pattern) 是一种创建型设计模式,它的目的是确保一个类只有一个实例,并提供一个全局访问点来访问该实例
核心要点
- 唯一性
类只有一个实例,避免重复创建多个对象导致资源浪费或状态不一致。 - 全局访问点
提供一个全局方法,方便访问唯一实例。
实现
基础要点
根据单例模式的特点,要求实现单例模式必须要构造器私有化,防止外界创建对象
饿汉式
顾名思义,该方式就是直接在类加载时就初始化单例实例
示例代码如下:
public class Singleton {// 静态变量在类加载时初始化private static final Singleton INSTANCE = new Singleton();// 私有化构造器,防止外部实例化private Singleton() {}// 提供全局访问点public static Singleton getInstance() {return INSTANCE;}
}
- 优点:实现简单,线程安全。
- 缺点:即使未使用该实例,也会初始化,占用内存。
懒汉式
也是顾名思义,就是“懒”,即在类加载时并不会立即创建实例,而是在外界需要时才会创建实例
示例代码如下:
public class Singleton {// 静态变量,延迟初始化private static Singleton instance;// 私有化构造器private Singleton() {}// 提供全局访问点public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
- 优点:按需创建,节省资源。
- 缺点:在多线程环境下存在安全问题(多个线程可能同时创建实例)
懒汉式(线程安全,双重检查锁定)
由于在高并发情况下,一般的懒汉式方案可能会出现线程安全问题,所以需要对其进行改造
改造后如下:
public class Singleton {// 使用 volatile 确保可见性和禁止指令重排序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;}
}
- 优点:线程安全,性能较高
- 缺点:实现复杂
这里补充一下 volatile 关键字:
volatile
是 Java 提供的一个关键字,用于修饰变量,确保变量在多线程环境中的可见性和禁止指令重排序。这些特性是为了解决并发编程中的常见问题,如线程间数据不一致和由于优化导致的潜在问题。
- 可见性
在多线程环境中,每个线程都有自己的工作内存(CPU 缓存),线程可能会将变量的副本缓存在自己的工作内存中,导致一个线程对变量的修改对另一个线程不可见。而使用volatile
修饰的变量,所有线程对其的读写操作直接发生在主内存中。当一个线程修改了 volatile
变量的值,其他线程能够立即看到最新值。
- 禁止指令重排序
为了优化性能,CPU 和编译器可能会对指令进行重排序(Instruction Reordering)。在单线程环境下,这种重排序不会改变程序的执行结果,但在多线程环境下,可能导致意想不到的问题,所以也得注意一下。volatile
通过加入内存屏障(Memory Barrier),确保指令在多线程环境中按程序的逻辑顺序执行。它可以防止变量的赋值操作被重排序到不合适的位置。
这里举一个例子:
示例代码:
class Singleton {private static Singleton instance;public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) {if (instance == null) { // 第二次检查instance = new Singleton(); // 问题出在这里}}}return instance;}
}
这里看到 instance 没有被 volatile
修饰,那么就可能会出现问题:
instance = new Singleton()
是一个分解过程,可能会被重排序成以下步骤:
- 分配内存空间。
- 将内存地址赋值给
instance
。 - 调用构造函数初始化对象。
如果发生了重排序,步骤可能变成:
- 先执行第 2 步,再执行第 3 步。
此时,另一个线程读取 instance
时,可能得到一个“未初始化完全”的对象。
静态内部类实现
利用类加载机制实现线程安全,推荐使用
public class Singleton {// 静态内部类private static class Holder {private static final Singleton INSTANCE = new Singleton();}private Singleton() {}// 通过静态内部类返回实例public static Singleton getInstance() {return Holder.INSTANCE;}
}
优点:
- 线程安全
- 延迟加载
- 实现简单
简单解读一下:
在外部类加载时,并不会主动加载内部类,而只有当外界主动使用内部类时,内部类才会被类加载器加载。借助这一特性可以实现延迟加载。并且,当类加载加载内部类时,由 JVM 保证线程的安全性,这样可以实现线程安全。总的来看,操作还是十分简单的。
使用枚举实现
使用 枚举(Enum) 是单例模式的一种推荐实现方式,尤其在 Java 环境中。枚举不仅能实现线程安全的单例,还能天然防止反射攻击和序列化漏洞,是最简洁且安全的单例实现方式。
示例代码:
public enum Singleton {INSTANCE; // 枚举类型的唯一实例// 示例方法public void doSomething() {System.out.println("Executing some logic in Singleton");}
}
优点:
- 简洁
- 天然线程安全
- 防止反射和序列化攻击
简单解读一下:
在 Java 中,每个枚举类型都是通过 java.lang.Enum
类实现的。枚举类的实例化是由 JVM 保证的,并且其类加载机制确保了线程安全性。每个枚举常量(如 INSTANCE
)在类加载时就会被初始化(饿汉式),且只会初始化一次。
通过反射可以破坏普通单例模式,调用私有构造器来创建新的实例。但枚举类型的构造器在 Java 中是隐式的,JVM 不允许通过反射调用枚举类型的构造器,因此无法创建多个实例。
普通单例在序列化和反序列化时可能生成多个实例(通过 ObjectInputStream
)。枚举类型的序列化由 JVM 自动处理,枚举单例在序列化时只会返回相同的枚举实例。
总结
多种实现方案的比较如下: