文章目录
-
- 单例模式1️⃣
-
- 特性💪
- 单例模式的类型与实现:
-
- 类型
- 懒汉式实现(线程不安全)
- [懒汉式实现(线程安全)](#懒汉式实现(线程安全))
- 双重锁校验懒汉式(线程安全)
- 饿汉式实现(线程安全)
- 使用类的内部类实现⭐
- 枚举方式实现单例(推荐)👍
单例模式1️⃣
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
特性💪
- 唯一性:整个系统中,单例类只能有一个实例。
- 私有化构造函数:防止外部通过new关键字直接创建对象。
- 静态方法提供全局访问点 :通常使用
getInstance()
方法来获取该类的唯一实例。 - 延迟实例化(懒加载) :有些实现会在第一次调用
getInstance()
时才创建实例,以节省资源。
单例模式的类型与实现:
类型
- 懒汉式:在真正需要使用对象时才去创建该单例类对象
- 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
懒汉式实现(线程不安全)
只有当调用getInstance()
方法时才会创建单例对象,这种方式实现了延迟加载,但是需要考虑多线程环境下的线程安全问题。
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 单例模式不允许外部直接创建,所以构造函数添加私有属性
private
- 这种方式满足懒汉式,但是在并发场景下,多个线程使用单例对象可能导致实例并存,从而违反了单例要求
懒汉式实现(线程安全)
上述懒汉式实现是线程不安全的(例如同时两个线程去获取单例对象,如果此时单例对象还未创建,可能会导致同事创建两个对象,从而违反单例),故我们要解决线程安全问题。
最容易想到的方法:使用锁(synchronized
)给类加锁来保证线程安全
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重锁校验懒汉式(线程安全)
但是上述方法每次获取对象的时候都要去先获取锁,并发性能不是很好
可以进行优化:(如果没有实例化则加锁创建,如果实例化了则直接获取,可以使得已经实例化的单例对象在获取单例对象时无需先获取锁,而是直接获取对象)
使用Double Check(双重校验) + Lock(加锁) 的写法:
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;
}
}
同时我们也使用了volatile
关键字去确保instance
变量的更新对所有线程都是立即可见的,并且禁止指令重排序 ,保证多线程环境下的正确性。
- 防止指令重排序 : 在多线程环境下,JVM为了优化性能可能会对指令进行重排序。对于单例对象的创建而言,构造函数内部的操作可能被重排序到对象引用赋值之后。例如,如果一个线程正在执行实例化操作,它可能会先将对象引用设置为非空(即指向一块内存),然后再完成对象的初始化。这种情况下,另一个线程可能看到的是一个部分初始化的对象(因为对象的引用不是
null
了),这会导致不可预测的行为或错误。volatile
关键字可以禁止这种指令重排序,保证对象完全初始化之后才会被其他线程看到。 - 可见性保证 :
volatile
关键字确保了一个线程对共享变量(在这个场景下是单例对象的引用)的修改对于其他线程是立即可见的。也就是说,当一个线程成功创建了单例对象后,所有其他线程都能看到这个对象已经被正确地初始化了,而不会读取到旧的或者默认的值(如null
)。这避免了多个线程同时创建多个实例的情况。
饿汉式实现(线程安全)
在类加载的时候就创建好单例对象,这种方式简单但不够灵活,因为它不能做到延迟加载。
java
public class Singleton{
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return singleton;
}
}
在类加载的时候,private static final Singleton singleton = new Singleton();
这行代码已经实例化好了一个单例对象在内存中。
使用类的内部类实现⭐
利用了Java语言的类加载机制,只有当调用getInstance()
方法时,内部类才会被加载,从而实现了懒加载和线程安全,同时不会因为加锁的方式耗费性能。
java
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载。
- 此种方式也是非常推荐使用的一种单例模式
枚举方式实现单例(推荐)👍
在Java中,使用枚举(Enum)来实现单例模式是一种非常简洁且高效的方法。枚举类型的单例不仅能够防止反射攻击和序列化导致的重复实例化问题,而且代码量极少,易于理解和维护。这是因为Java的枚举机制保证了每个枚举常量的唯一性 ,并且在类加载时自动初始化。
java
public enum Singleton {
INSTANCE;
private final String property;
// 初始化属性值
Singleton() {
this.property = "Some Value";
}
public String getProperty() {
return property;
}
// 其他业务方法
public void doSomething() {
// 方法逻辑...
}
}
public class Client {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.doSomething();
System.out.println(singleton.getProperty());
}
}
枚举单例的优点:
- 天然线程安全:由于枚举常量在类加载时就被初始化,所以不需要额外的同步代码来保证线程安全。
- 防止反序列化创建新实例:枚举类型具有内在的序列化机制,如果尝试反序列化一个枚举类型的对象,它总是返回现有的枚举常量,而不会创建新的实例。
- 防止反射攻击:即使通过反射调用私有构造函数,也无法创建新的枚举实例。
- 简洁:相比于其他单例模式的实现方式,枚举单例的代码更加简洁明了。
- 延迟加载:虽然枚举类型不是天生支持懒加载,但是可以通过将实际的工作委托给另一个静态内部类来实现这一点。