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;
}
}
饿汉模式解释如下:
- private 控制这个对象的访问权限,final 代表这个对象一旦创建就不能修改,static 利用了 JVM 类加载的机制,对象创建完成的时机是在类加载的初始化阶段,并且 instance 这个变量是存储在方法区(元空间)的。
- 私有化构造函数,使得外部的类无法通过 new 的方式获取。
- 单例模式需要被使用,就需要提供一个对外的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() 的时候才会创建这样的一个对象实例。但是这是一个线程不安全的单例,在多线程并发的情况下,会出现数据不同步的问题。
饿汉模式场景
在很多电商场景,如果这个数据是经常访问的热点数据,那我就可以在系统启动的时候使用饿汉模式提前加载(类似缓存的预热)这样哪怕是第一个用户调用都不会存在创建开销,而且调用频繁也不存在内存浪费了。
懒汉模式场景
而懒汉式呢我们可以用在不怎么热的地方,比如那个数据你不确定很长一段时间会不会有人会去调用这个实例,那就用懒汉,如果你使用了饿汉,但是过了几个月还没人调用,提前加载的类在内存中是有资源浪费的。
线程安全问题
上面的懒汉没加锁,大家肯定都知道懒汉的线程安全问题的吧?
在运行过程中可能存在这么一种情况:多个线程去调用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;
}
带着下面几个疑问,继续往下看?
- 为什么要使用 volatile?
- 为什么要进行 double check?
- 为什么要在同步块外面加一层if判断?
- 为什么要在同步块里面又加一层if判断?
- 什么是指令重排序?
- 为什么要使用 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,禁止指令的重排序,保证程序能正常运行。
- 为什么要进行 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 类型的变量。
相关文章