单例模式(Singleton)
懒汉模式
确保只有一个实例,并且提供实例的全局访问点
懒汉模式
1、懒汉模式-线程不安全
public class LazySingleton_00 {
private static LazySingleton_00 lazySingleton00;
// 构造方法私有化
private LazySingleton_00() {
}
public static LazySingleton_00 getLazySingleton() {
if (Objects.isNull(lazySingleton00)) {
return lazySingleton00;
}
lazySingleton00 = new LazySingleton_00(); // 非线程安全
return lazySingleton00;
}
}
2、懒汉模式-线程安全
public class LazySingleton_01 {
private static LazySingleton_01 lazySingleton01;
// 构造方法私有话
private LazySingleton_01() {
}
public static synchronized LazySingleton_01 getLazySingleton() {//临界区
if (Objects.isNull(lazySingleton01)) {
return lazySingleton01;
}
lazySingleton01 = new LazySingleton_01();
return lazySingleton01;
}
}
从案例1,可以发现创建对象都是通过共有的一个方法获取对象的。当在多线程的场景下,就会出现线程安全问题。想想如果有多个线程进入if (Objects.isNull(lazySingleton01))
,并且此时lazySingleton00为null ,那么就会有多个线程同时创建LazySingleton_00实例。这样就导致了多次实例化。
案例2,使用synchronized对方法进行加锁后,在一个时间点只能有一个线程进入该方法,从而避免了类的多次实例化。
但是会发现一个问题,当一个线程进入该方法后,其他试图进入该方法的线程必须等待,即使已经被实例化了。这样就会导致性能问题,对象只创建一个,但是之后的使用都是需要进入synchronized方法。
3、懒汉模式-双重检查锁
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法,称为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
public class LazySingleton_03 {
private static LazySingleton_03 lazySingleton03 = null;
private LazySingleton_03() {
}
public static LazySingleton_03 getLazySingleton03() {
if (Objects.isNull(lazySingleton03)) {
synchronized (LazySingleton_03.class) {
if (Objects.isNull(lazySingleton03)){
// 1.分配空间 2.初始化 3.引用赋值
lazySingleton03 = new LazySingleton_03();
}
}
}
return lazySingleton03;
}
}
这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()行代码,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
4、懒汉模式-双重检查锁+volatile
public class LazySingleton_04 {
private volatile static LazySingleton_04 lazySingleton03 = null; //声明成 volatile
private LazySingleton_04() {
}
public static LazySingleton_04 getLazySingleton03() {
// 线程B执行到此处
if (Objects.isNull(lazySingleton03)) {
synchronized (LazySingleton_04.class) {
if (Objects.isNull(lazySingleton03)){
// 1.分配空间 2.初始化 3.引用赋值
lazySingleton03 = new LazySingleton_04();
// 字节码层
// JIT , CPU 有可能对如下指令进行重排序
// 1 .分配空间
// 2 .初始化
// 3 .引用赋值
// 如重排序后的结果为如下
// 1 .分配空间
// 3 .引用赋值 如果在当前指令执行完,有其他线程来获取实例,将拿到尚未初始化好的 实例
//############# 线程A执行到这里 #################
// 2 .初始化
}
}
}
return lazySingleton03;
}
}
这里使用volatile作用是:禁止指令重排序优化。这样可以保证对线程环境下也能够正常运行。
饿汉模式
public class HungrySingleton{
private static HungrySingleton instance=new HungrySingleton();
private HungrySingleton(){
//防止反射攻击
if (HungrySingleton.instance != null){
throw new RuntimeException("单例不允许多个实例!");
}
}
public static HungrySingleton getInstance() {
return instance;
}
}
类加载的时候就创建对象,不管使不使用都会创建。
单例模式-静态内部类实现
静态内部类(Static Inner Class)在 Java 中的加载时机与其外部类不同。具体来说,静态内部类在第一次使用时才会被加载,这与外部类的加载时机无关。这种特性使得静态内部类常用于实现单例模式的延迟加载(Lazy Initialization)。
静态内部类的加载时机
1.外部类加载时,静态内部类不会被立即加载。静态内部类只有在第一次被访问或其成员(如字段、方法)被使用时,才会进行加载和初始化。
2.静态内部类的加载遵循类加载器的规则,且仅在需要时才加载。这与外部类的加载是相互独立的,外部类的实例化、方法调用等操作不会触发静态内部类的加载。
public class InnerClassSingleton{
public static void main(String[] args) {
InnerClassSingleton instance = InnerClassSingleton.getInstance();
InnerClassSingleton instance1 = InnerClassSingleton.getInstance();
System.out.println(instance == instance1);
}
//初始化之前不会加载静态内部类,是在调用get方法并且返回值的时候初始化。
// 静态内部类,持有 InnerClassSingleton 的唯一实例
private static class InnerClassHolder{
// 静态变量,存放唯一的 instance 实例
private static InnerClassSingleton instance= new InnerClassSingleton();
}
private InnerClassSingleton(){
//防止反射攻击
if (InnerClassHolder.instance != null){
throw new RuntimeException("单例不允许多个实例!");
}
}
public static InnerClassSingleton getInstance(){
return InnerClassHolder.instance;// 调用时,触发 InnerClassHolder 加载
}
}
当InnerClassSingleton类被加载时,静态内部类InnerClassHolder没有被加载。只有当被调用getInstance()方法从而触发InnerClassHolder.instance时,内部类才会加载,这个时候就会对instance进行实例化。并且JVM保证了 静态内部类在第一次使用时才会被加载。
也是懒加载的一 种形式。
单例模式-枚举实现
Java 枚举类型实际上是继承自 java.lang.Enum 的特殊类。每个枚举值(常量)是该枚举类型的一个实例,枚举实例的创建过程由 Java 编译器和 JVM 内部控制。在枚举被加载时,所有的枚举常量会被实例化,而每个枚举常量都是 final 和 static 的,因此只会被实例化一次。
public enum EnumSingleton {
INSTANCE;
public void print(){
System.out.println(this.hashCode());
}
public static void main(String[] args) {
EnumSingleton instance = EnumSingleton.INSTANCE;
EnumSingleton instance1 = EnumSingleton.INSTANCE;
System.out.println(instance1 == instance);
EnumSingleton.INSTANCE.print();
}
}