单例模式详解
为什么要有单例模式
单例模式是一种创建型设计模式,旨在确保一个类在整个应用程序生命周期中只有一个实例,并提供一个全局访问点。通过单例模式,可以避免多个实例带来的资源浪费和数据不一致问题。
关键点:
-
唯一性:确保一个类只有一个实例。
-
全局访问:提供一个全局的访问点来获取该实例。
单例模式的意图
-
控制实例数量:限制类的实例化数量,通常为一个。
-
全局访问点:提供一个统一的接口来访问该实例,方便管理和使用。
-
节约资源:避免重复创建实例,节约系统资源。
单例模式有哪些实现
主要有饿汉,懒汉,双重检查锁,枚举这几种
怎么才能实现单例呢?一半就是将这个对象创建的实例作为这个对象的属性,每次就去获取这个属性就可以了
首先看看饿汉式
之所以叫饿汉,我的理解是饿汉比较心急,所以会在一开始就去创建一个实例而不等到需要调用时才去
java
public class huangryman {
private static huangryman instance = new huangryman();
private huangryman (){}
public static huangryman getInstance() {
return instance;
}
}
这个代码初始化后就会调用私有的构建函数去new 一个huangryman
但他会导致可能导致资源浪费,尤其在实例未被使用时。
再看看懒汉式
之所以叫懒汉式,懒汉比较懒,所以他会在第一次需要获取实例才去创建
java
public class Singleton {
private static Singleton instance;
private Singleton() {
// 私有构造函数
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
instance = new Singleton();
}
return instance;
}
}
这里看到这个instance没有所有一来就去new,而是在get的时候去检测instance是否为空,也就是这个实例是否被创建,如果没被创建就会去创建,如果创建了就直接返回那个实例
但是这里有个问题,这里有个 get 和 creat,其实就是 读 和 写 ,那么读写结合起来最常见的问题就是一个多线程下的并发问题。
所以这里我们引入双重检查锁
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 = new Singleton()
操作的三个步骤(分配内存、初始化对象、建立引用)按顺序执行 -
保证可见性:当一个线程修改了 instance 变量,其他线程能立即看到最新值
-
第一重检查:避免不必要的同步开销
-
同步块:保证只有一个线程能进入创建实例的代码段
-
第二重检查:防止多个线程同时通过第一重检查后重复创建实例
其实双重检查锁也是一种懒汉模式,因为他也是在被调用时加载,但通过一些方式去确定了线程的安全而已
那还有没有办法可以实现单例呢,下面提供两个不容易想到的
静态内部类
java
public class Singleton {
private Singleton() {
// 私有构造函数
}
private static class SingletonHelper {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
- 当
getInstance()
首次被调用时,SingletonHelper
类才会被加载并初始化其静态成员INSTANCE
。 JVM 的类加载过程是 线程安全 的,保证静态变量初始化操作只会执行一次。
我觉得 静态内部类 是对饿汉的一个优化。
把 private static final Singleton INSTANCE = new Singleton();放在内部类里实现了懒加载。
静态内部类实现是《Effective Java》作者Joshua Bloch推荐的方式,它在安全性、性能和代码简洁性之间取得了完美平衡,是大多数场景下的最佳选择。如果不需要延迟初始化,枚举实现(enum
)则是更优解。
枚举实现
再说下枚举实现
Java中有个枚举类叫做Enum
枚举类默认继承 java.lang.Enum
,无法再继承其他类(但可以实现接口)。
-
当枚举类首次被引用时(如
Singleton.INSTANCE
),JVM 会加载并初始化该类。 -
类加载的线程安全性:JVM 保证类加载过程(包括静态变量的初始化)是线程安全的,且只会执行一次。
-
枚举常量的初始化 :所有枚举常量会在类加载的
<clinit>
方法中被创建,且每个枚举常量只会被初始化一次。
java
public enum Singleton {
INSTANCE; // 单例实例
}
直接调用Singleton.INSTANCS就ok了
但是枚举类会在首次被引用时立即初始化,如果实例化过程耗时较长,可能影响启动性能。