文章目录
1 单例模式(静态内部类方式实现 懒汉)
class Singleton{
// 构造方法私有化,防止被其他类创建对象
private Singleton(){}
public static class LazyLoader{
// 静态内部类中创建 静态的Singleton对象
private static final Singleton INSTANCE = new Singleton();
}
// 对外只暴露一个方法,用于获取该单例对象
public static Singleton getInstance(){
// 1、外部类class Singleton初始化,不会触发静态内部类static class LazyLoader的初始化
// 2、只有 当 其他类调用getInstance方法获取INSTANCE时
// ,才会创建Singleton实例,触发初始化————>【懒汉】
// 解释: 由于INSTANCE是 静态常量中的引用类型
// 因此在该对象创建前,才会触发该类(静态内部类)LazyLoader的加载和初始化
// 而静态内部类LazyLoader在加载时,是线程安全的
// ——>也就保证了Singleton对象被创建的时候,不会出现线程安全问题
return LazyLoader.INSTANCE;
}
}
2 双重检查锁Double Check (懒汉式)
/**
* @description: 双重检查锁 创建单例模式
* @param: null
* @return:
* @author HUANGXUAN
* @date: 2021/7/20 21:23
*/
class DoubleCheckSingleton{
// 私有构造方法,防止其他类创建对象
private DoubleCheckSingleton() {}
// 为何使用 volatile关键字修饰? ——>
// 1、禁用CPU指令重排序,防止以下代码编译后的执行顺序变化
// 2、保证共享变量的可见性——>缓存一致性协议——>开启总线嗅探机制
private static volatile DoubleCheckSingleton SINGLETON;
// 双重检查锁的精华部分:
// 当其他类调用该类的getInstance方法时,会发生以下情况:
private static DoubleCheckSingleton getInstance(){
// 第一道关卡 用于优化执行效率——>
// 因为如果synchronized加在getInstance方法上
// 当多个线程同时调用getInstance方法时,一个线程拿到锁,会让其他线程陷入阻塞,降低效率
if (SINGLETON == null){
synchronized (DoubleCheckSingleton.class){
if (SINGLETON == null){
SINGLETON = new DoubleCheckSingleton();
}
}
}
return SINGLETON;
}
}
DoubleCheck实现过程分析:
T1线程调用时,发现SINGLETON == null,于是进入第一道关卡,
顺理成章拿到锁,进入第二道关卡(SINGLETON == null),
开始进入创建SINGLETON对象的时刻(加载 连接 初始化...)---------------> (一)
如上,在T1线程过完第一道关卡,拿到锁,把创建对象的代码块锁住时,
如果T2线程是紧跟着T1线程调用getInstance的话,它也会进入第一道关卡
但由于创建对象的代码块被锁住了,于是他只能进入阻塞状态
接着当T1线程创建完对象出来后,return SINGLETON;
T2线程被唤醒,拿到锁,进行第二道判断SINGLETON == null—> 发现SINGLETON对象不为空了
所以T2线程直接返回 T1线程创建的SINGLETON对象,这就保证了【单例模式】
如果Instance 不加volatile 则会出现——> 【返回半初始化对象】的情况:
有一种情况是:
第一个调用getInstance方法的T1线程在【刚进入同步代码块,执行对象创建时,即 (一)时刻】
由于对象创建是需要经过(加载——>连接——>调用构造方法并初始化)三个步骤,
然后再将创建好的对象的引用地址,赋值给SINGLETON这个变量。此时SINGLETON才 !=null
【由于没加volatile 去禁用CPU指令重排序!
因此赋值引用地址给SINGLETON这个变量的操作:putstatic
【可能会由于重排序,而提前到 调用构造方法和初始化:invokespecial之前!】
因此此时 if判断出SINGLETON确实也不为空了。他就会认为对象已经创建完成
而后:后面的return实例的操作就会返回一个半初始化的对象】
!!! 那么问题来了??直接return? 可是这个时候,SINGLETON变量指向的对象还没有初始化完成!
也就是说,这个对象它才生出来一半,还有一半没生出来——> 即【返回一个半初始化的对象】
volatile 是如何解决问题的?
加了volatile 则可以解决这个问题。
因为Instance 在if(Instance == null)这个读操作中,volatile 会加入一个读屏障!
保证读屏障之后的每次共享变量读取,都会去主内存中读取!并且防止读屏障以后的代码被重排序到它前面去!
而在Instance = new Singleton 这个创建对象并赋值的写操作中,则会加入写屏障。
防止写屏障之前的代码,被重排序到后面去!
3 饿汉模式
public final class Singleton implements Serializable {
private Singleton() {}
这样初始化是否能保证单例对象创建时的线程安全?
// 是线程安全的。因为static修饰的变量赋值操作,会被整合成一个cinit方法。在初始化阶段执行。
//此时是线程安全的
private static final Singleton INSTANCE = new Singleton();
为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
// 可以用于实现懒惰的初始化,预留给后期进行修改扩展,还可以在创建单例时有更多控制
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
为什么加 final?
防止被继承重写破坏
为什么设置为私有? 是否能防止反射创建新的实例?
防止其他类调用他的构造方法。反射不能防止创建新的实例。反射可以通过暴力破解方式,
拿到私有的构造方法
怎么防止反序列化破坏单例模式?
该单例类如果implements Serializable。
当使用反序列化创建对象时,会调用readResovle方法,返回的将不是我们要的单例对象。
因此我们直接重写该方法,返回需要的单例对象
public Object readResovle(){
return INSTANCE;
}
避免反序列化获取对象时,使用他默认的方法
4 枚举方式——饿汉模式
问题1:枚举单例是如何限制实例个数的
// 反编译得出:该INSTANCE就是枚举类种的一个静态成员变量。 static enum INSTANCE;
问题2:枚举单例在创建时是否有并发问题
// 不会有并发线程安全问题。因为他也是static成员变量
问题3:枚举单例能否被反射破坏单例
// 【好处】 不能用反射破坏单例
问题4:枚举单例能否被反序列化破坏单例
// 枚举类默认实现序列化接口!
// 不会被反序列化破坏单例。不用自己做额外操作。
问题5:枚举单例属于懒汉式还是饿汉式
// 饿汉
问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
// 可以写一个构造方法,把初始化的逻辑加入进去即可
enum Singleton {
INSTANCE;
}
5 懒汉式
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
分析这里的线程安全, 并说明有什么缺点
// 线程安全。但是效率低。 同步代码块太长了。每次调用getInstance时都会直接陷入阻塞
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}