阅读 47

设计模式之单例模式

1. 前言

为了限制该类对象被随意的创建,需要保证该类构造方法是私有的,这样外部类就无法创建该类型的对象了,另外,为了方便给客户对象提供对此单例对象的使用,给它提供一个全局访问点。

2. 重新认识单例模式

2.1 什么是单例模式

首先给单例下一个定义:在当前进程中,通过单例模式创建的类有且只有一个实例

抛开 CPU 的缓存不谈,单例的本质是在进程的所分配的内存中仅能存在唯一的一个对象,而单例模式就是采用一种手段或者方法,使得这个对象在内存中是唯一存在的。

从 Java 的层面讲,对象的实例分配在堆区,而我们所需要保证的是在这个堆内存区域,通过 Class 所创建的这个对象的全局唯一性。

2.2 为什么需要单例模式

那么我们为什么需要使用单例模式呢?显而易见的是,单例模式保证了内存中全局的唯一性,避免了对象实例的重复创建,节约了系统资源。但是它的缺点也是比较明显的,没有接口,不能被继承,并且也违反单一职责的原则(一个单例往往使用的业务场景比较多,试想,一个单例负责一个功能,它对系统资源的使用率势必会下降)。

单例有如下几个特点:

  • 在Java应用中,单例模式能保证在一个JVM中,该对象只有一个实例存在
  • 构造器必须是私有的,外部类无法通过调用构造器方法创建该实例
  • 没有公开的set方法,外部类无法调用set方法创建该实例
  • 提供一个公开的get方法获取唯一的这个实例

那单例模式有什么好处呢?

  • 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销
  • 省去了new操作符,降低了系统内存的使用频率,减轻GC压力
  • 系统中某些类,如spring里的controller,控制着处理流程,如果该类可以创建多个的话,系统完全乱了
  • 避免了对资源的重复占用

2. 单例模式实现

2.1 饿汉模式

实现代码如下:

package com.wxw.singleton;

/**
 * @Author: 公众号:Java半颗糖
 * @desc: 1. 饿汉模式
 * @create: 2019-10-20-20:20
 */
public class HungrySingleton {
  
    //【1】保存该类对象的实例,饿汉式的做法:在声明的同时初始化该对象
    private static final HungrySingleton instance = new HungrySingleton();
  
    //【2】将构造函数私有化,不对外提供构造函数
    private HungrySingleton() {
    }
   
    //【3】对外提供访问该类对象的方法
    public static HungrySingleton getInstance() {
        return instance;
    }
}
复制代码

饿汉模式解释如下:

  1. private 控制这个对象的访问权限,final 代表这个对象一旦创建就不能修改,static 利用了 JVM 类加载的机制,对象创建完成的时机是在类加载的初始化阶段,并且 instance 这个变量是存储在方法区(元空间)的。
  2. 私有化构造函数,使得外部的类无法通过 new 的方式获取。
  3. 单例模式需要被使用,就需要提供一个对外的public接口

它基于 classloder 机制避免了多线程的同步问题,没有加锁,执行效率会提高。但是饿汉式的缺点也是非常明显,即便我们没有去使用这个对象,也会在这个类加载时就初始化这个实例,比较浪费内存。

2.2 懒汉模式

前面说过饿汉式的的缺点是对内存资源的浪费,那么有没有一种机制,能够实现延迟初始化,只有在我们需要的时候进行创建。

有的,下面先看懒汉式单例中线程不安全的实现

package com.wxw.singleton;

/**
 * @Author: 公众号:Java半颗糖
 * @desc: 2. 懒汉模式
 * @create: 2019-10-20-20:53
 * @detail:
 */
public class LazySingleton {

    private static LazySingleton instance;  // ----> 注释1

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            //  ----> 注释2
            instance = new LazySingleton(); // ----> 注释3
        }
        return instance;
    }
}
复制代码

懒汉单例实现了延迟初始化,只有在 getInstance()  的时候才会创建这样的一个对象实例。但是这是一个线程不安全的单例,在多线程并发的情况下,会出现数据不同步的问题。

饿汉模式场景

在很多电商场景,如果这个数据是经常访问的热点数据,那我就可以在系统启动的时候使用饿汉模式提前加载(类似缓存的预热)这样哪怕是第一个用户调用都不会存在创建开销,而且调用频繁也不存在内存浪费了。

懒汉模式场景

而懒汉式呢我们可以用在不怎么热的地方,比如那个数据你不确定很长一段时间会不会有人会去调用这个实例,那就用懒汉,如果你使用了饿汉,但是过了几个月还没人调用,提前加载的类在内存中是有资源浪费的。

线程安全问题

上面的懒汉没加锁,大家肯定都知道懒汉的线程安全问题的吧?

image.png 在运行过程中可能存在这么一种情况:多个线程去调用getInstance方法来获取Singleton的实例,那么就有可能发生这样一种情况,当第一个线程在执行if(instance==null)时,此时instance是为null的进入语句。

在还没有执行instance=new Singleton()时(此时instance是为null的)第二个线程也进入了if(instance==null)这个语句,因为之前进入这个语句的线程中还没有执行instance=new Singleton(),所以它会执行instance = new Singleton()来实例化Singleton对象,因为第二个线程也进入了if语句所以它会实例化Singleton对象。

这样就导致了实例化了两个Singleton对象,那怎么解决?加锁

package com.wxw.singleton;

/**
 * @author 公众号:Java半颗糖
 * @desc:
 * @date: 2021/7/22
 */
public class ThreadSafeLazySingleton {

    private static ThreadSafeLazySingleton instance;

    // 私有构造方法,防止被实例化
    private ThreadSafeLazySingleton() {
    }

    // 静态get方法
    public static synchronized ThreadSafeLazySingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeLazySingleton();
        }
        return instance;
    }
}
复制代码

这是一种典型的时间换空间的写法,不管三七二十一,每次创建实例时先锁起来,再进行判断,严重降低了系统的处理速度。

有没有更好的处理方式呢?可以使用双重检测机制(DCL)实现单例

2.3 双锁检测(double check lock)

package com.wxw.singleton;

/**
 * @Author: 公众号:Java半颗糖
 * @desc: 双重检测 DCL
 * @create: 2019-10-20-20:47
 */
public class DoubleCheckSingleton {
    
    // 【1】volatile 保证可见性和禁止指令重排序
    private volatile static DoubleCheckSingleton singleton = null;
    // 私有化构造方法
    private DoubleCheckSingleton() {
    }

    public static DoubleCheckSingleton getInstance() {
        // 【2】先检查实例是否存在,如果不存在才进入下面的同步块
        if (singleton == null) {
            // 【3】线程安全的创建实例
            synchronized (DoubleCheckSingleton.class) {
                【4】再次检查实例是否存在,如果不存在才真正的创建实例
                if (singleton == null) {
                    // 【5】禁止指令重排序
                    singleton = new DoubleCheckSingleton();
                }
            }
        }
        return singleton;
    }
复制代码

带着下面几个疑问,继续往下看?

  1. 为什么要使用 volatile?
  2. 为什么要进行 double check?
    1. 为什么要在同步块外面加一层if判断?
    2. 为什么要在同步块里面又加一层if判断?
  3. 什么是指令重排序?
  1. 为什么要使用 volatile?
  • 防止指令重排序,因为instance = new Singleton()不是原子操作
  • 保证内存可见

让我们来看一下这行代码: instance = new DoubleCheckSingleton() ,执行完这一行代码可以分成三个步骤:

  • step1:在内存中分配一块空间。
  • step2:对内存空间进行初始化。
  • step3:把对象在内存中的位置指向 instance

如果按照 CPU 或者 JIT 编译器能够按照片正常的指令执行的话,是不需要 volatile,但是 CPU 和 JIT 即时编译器为了能获得性能上的提升,往往会对字节码指令进行重排序,这就会导致 step2 和 step3 执行的顺序颠倒。执行步骤就变成了:

  • step1:在内存中分配一块空间。
  • step3:把对象在内存中的位置指向 instance
  • step2:对内存空间进行初始化。

举例分析

现在假设有两个线程T1、T2,T1 线程执行完重排序后的 step3 ,CPU 的执行权被 T2 获得。这个时候,instance 已经不为 null 了,他指向了内存中的一块地址。T2 执行到第一个 if 的时候,发现 instance 不为 null,就直接返回,但是这个 instance 并没有被初始化,这就会导致 T2 在执行的过程中发生不可预知的错误。

通过volatile修饰的变量,不会被线程本地缓存,所有线程对该对象的读写都会第一时间同步到主内存,从而保证多个线程间该对象的准确性

小节

DCl 单例中,instance = new DoubleCheckSingleton() 分为三步:1、分配内存空间,2、初始化对象,3、设置instance指向被分配的地址。然而指令的重新排序,可能优化指令为1、3、2的顺序。如果是单个线程访问,不会有任何问题。但是如果两个线程同时获取getInstance,其中一个线程执行完1和3步骤,此时其他的线程可以获取到instance的地址,在进行if(instance==null)时,判断出来的结果为false,导致其他线程直接获取到了一个未进行初始化的instance,这可能导致程序的出错。

所以用volatile修饰instance,禁止指令的重排序,保证程序能正常运行

  1. 为什么要进行 double check?

为什么要使用 double check? 现假设有两个 T1 和 T2,T1 执行到注释2处,CPU 的执行权被 T2 抢夺走,T2 执行完成之后创建了一个对象实例,并且释放 Java 的类锁。这个时候 T1 又重新获得了 CPU 的执行权,并且获得了类锁。如果没有第二个 if 的判断,T1 又会重新创建一个 实例对象,这样就破坏了单例。

明白了为什么会有第二个 if ,现在来看为什么会有第一个 if?其实不难看懂,现在假设有一个线程 T3 ,如果没有第一个 if,它就会直接尝试获取锁资源。要知道,锁资源是非常宝贵的,如果每个线程一来就直接申请锁资源,而不是先对 instance 进行判断,这势必会对程序的性能造成影响。

2.4 静态内部类

静态内部类的实质是利用了 JVM 的加载机制,它的本质和饿汉式是一样的。下面来看具体的代码实现:

package com.wxw.singleton;

/**
 * @Author: 公众号:Java半颗糖
 * @desc: 静态内部类的单例
 * @create: 2019-10-20-21:01
 */
public class InnerClassSingleton {

    // 私有化构造方法
    private InnerClassSingleton() {
    }

    // 获取单例
    public static InnerClassSingleton getInstance() {
        return SingletonFactory.INSTANCE;
    }

    /* 此处使用一个内部类来维护单例 */
    private static final class SingletonFactory {
        private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }
}
复制代码

道理其实是很简单的,作为一个私有的静态内部类,它关闭了对外实例化的接口,而在它的内部,实例化了一个外部类的对象。那么这个 InnerClassSingleton 在什么时候会被实例化呢?只有在它调用 getInstance()的时候才会被初始化,这是不是就实现了延迟初始化呢?

2.5 枚举单例

写完了上面的四种单例,现在来深入思考一下,上面的单例真的是安全的吗?也就是说难道真的没有办法对它们的单例进行破坏了吗?下面我用 Double Check 来进行验证说明,来看具体的代码实现:

反射破坏单例

通过反射获得单例类的构造函数,由于该构造函数是private的,通过setAccessible(true)指示反射的对象在使用时应该取消Java 语言访问检查,使得私有的构造函数能够被访问,这样使得单例模式失效。

@SneakyThrows
private static void test_reflect() {
    DoubleCheckSingleton checkSingleton01 = DoubleCheckSingleton.getInstance();
    Constructor<DoubleCheckSingleton> constructor = 
               DoubleCheckSingleton.class.getDeclaredConstructor();
    // 通过反射获得单例类的构造函数,由于该构造函数是private的,
    // 通过setAccessible(true)指示反射的对象在使用时应该取消
    // Java 语言访问检查,使得私有的构造函数能够被访问,这样使得单例模式失效
    constructor.setAccessible(true);
    DoubleCheckSingleton checkSingleton02 = constructor.newInstance();

    System.out.println(checkSingleton01.hashCode()); // 1625635731
    System.out.println(checkSingleton02.hashCode()); // 1580066828
}
复制代码

解决思路: 如果要抵御这种攻击,要防止构造函数被成功调用两次。需要在构造函数中对实例化次数进行统计,大于一次就抛出异常。

package com.wxw.singleton.reflect;

import lombok.SneakyThrows;

import java.lang.reflect.Constructor;

/**
 * @Author: 公众号:Java半颗糖
 * @desc: 双重检测 DCL
 * @create: 2019-10-20-20:47
 */
public class DoubleCheckSingletonByReflect {

    private volatile static DoubleCheckSingletonByReflect singleton;
    private static int count = 0; // 统计创建实例的次数

    private DoubleCheckSingletonByReflect() {
        synchronized (DoubleCheckSingletonByReflect.class) {
            if (count > 0) {
                throw new RuntimeException("创建了两个实例");
            }
            count++;
        }
    }

    public static DoubleCheckSingletonByReflect getInstance() {
        if (singleton == null) {
            synchronized (DoubleCheckSingletonByReflect.class) {
                if (singleton == null) {
                    singleton = new DoubleCheckSingletonByReflect();
                }
            }
        }
        return singleton;
    }

    @SneakyThrows
    public static void main(String[] args) {
        Constructor<DoubleCheckSingletonByReflect> declaredConstructor =
                DoubleCheckSingletonByReflect.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        DoubleCheckSingletonByReflect s1 = declaredConstructor.newInstance();
        DoubleCheckSingletonByReflect s2 = declaredConstructor.newInstance();

    }
}
复制代码

反序列化破坏单例

/**
 * implements Serializable
 * 注意:测试序列化时 需要被序列化的类 实现序列化接口
 */
@SneakyThrows
private static void test_serialize() {
    // 1、序列化
    DoubleCheckSingleton checkSingleton01 = DoubleCheckSingleton.getInstance();
    FileOutputStream fileOutputStream = new FileOutputStream("single");
    ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
    oos.writeObject(checkSingleton01);
    oos.flush();
    oos.close();

    // 2、反序列化
    // 破坏原因:反序列化的时候通过反射newInstance 重新生成了一个类 所以和原有的类不是同一个
    FileInputStream inputStream = new FileInputStream("single");
    ObjectInputStream oosInput = new ObjectInputStream(inputStream);
    DoubleCheckSingleton checkSingleton02 =
                      (DoubleCheckSingleton)oosInput.readObject();

    // 打印输出
    System.out.println("checkSingleton01 = " + checkSingleton01);
    System.out.println("checkSingleton02 = " + checkSingleton02);
    System.out.println(checkSingleton02 == checkSingleton01);
}
复制代码

注意

在测试反序列化破坏单例时,需要先序列化对象bean,序列化的前提是这个bean需要实现 Serializable 接口

解决思路

在单例实现类中,实现readResolve 方法

/**
 * 解决序列化和反序列化对单例的破坏
 * @return
 */
private Object readResolve() {
    return singleton;
}
复制代码

通过对 double check 验证,我们知道了它可以通过反射获取到对象实例,说明这也不是一个安全的单例。那么有没有安全的单例了,可以防止被序列化和反射?

有的,可以通过枚举来实现!

最后来看看枚举单例的实现,这是 Effective Java 这本书的作者的推荐写法:

/**
 * @author 公众号:Java半颗糖
 * @desc: 枚举实现单例模式
 * @date: 2021/7/20
 */
public enum EnumSingleton {

    INSTANCE;

    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}
复制代码

为什么序列化和反射的方式无法破坏枚举类型的单例呢?

任何的枚举类都会继承 Enum 这个抽象类,来看一下 Enum 的源码:

/**
 * prevent default deserialization
 */
private void readObject(ObjectInputStream in) throws IOException,
    ClassNotFoundException {
    throw new InvalidObjectException("can't deserialize enum");
}
复制代码

同样,在进行反射的过程也会对枚举类型进行判断,如果是枚举类型,就会直接抛出异常。这似乎解决了我们上面的问题,直接抛出异常来阻止反射和序列化的破坏枚举单例。

但是这还没有完,为什么 JVM 不支持对枚举的反射和序列化呢,而是使用抛异常的方式来阻止它?可以猜测一下,既然不支持,也就是说枚举类必然是不同于和他其他的类,那么枚举类的本质是什么呢?

通过 javap 命令可以得到字节码指令,字节码指令如下:(截取部分)

mac@wxw singleton % javap -c EnumSingleton.class 
Compiled from "EnumSingleton.java"
public final class com.wxw.singleton.EnumSingleton extends 
## 对 枚举单例的描述
java.lang.Enum<com.wxw.singleton.EnumSingleton> {
  ## 对枚举 变量的描述
  public static final com.wxw.singleton.EnumSingleton INSTANCE;

  public static com.wxw.singleton.EnumSingleton[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[Lcom/wxw/singleton/EnumSingleton;
       3: invokevirtual #2                  // Method "[Lcom/wxw/singleton/EnumSingleton;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[Lcom/wxw/singleton/EnumSingleton;"
       9: areturn

  public static com.wxw.singleton.EnumSingleton valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class com/wxw/singleton/EnumSingleton
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4                  // class com/wxw/singleton/EnumSingleton
       9: areturn

  public static com.wxw.singleton.EnumSingleton getInstance();
    Code:
       0: getstatic     #7                  // Field INSTANCE:Lcom/wxw/singleton/EnumSingleton;
       3: areturn

  static {};
    Code:
       0: new           #4                  // class com/wxw/singleton/EnumSingleton
       3: dup
       4: ldc           #8                  // String INSTANCE
       6: iconst_0
       7: invokespecial #9                  // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #7                  // Field INSTANCE:Lcom/wxw/singleton/EnumSingleton;
      13: iconst_1
      14: anewarray     #4                  // class com/wxw/singleton/EnumSingleton
      17: dup
      18: iconst_0
      19: getstatic     #7                  // Field INSTANCE:Lcom/wxw/singleton/EnumSingleton;
      22: aastore
      23: putstatic     #1                  // Field $VALUES:[Lcom/wxw/singleton/EnumSingleton;
      26: return
}

复制代码

枚举本质上是一个 final 类型类,它的变量都是 static、final 类型的变量。

相关文章

  1. 设计模式深入解析----单例模式
  2. 设计模式系列 - 单例模式
文章分类
后端
文章标签