设计模式(七): 单例模式(懒汉、饿汉、静态内部类、双重检验锁、枚举、序列化反序列化、反射攻击、容器单例)

1. 定义

在这里插入图片描述
优点
在这里插入图片描述
缺点
在这里插入图片描述
特性:
(1)私有构造函数
(2)线程安全
(3)延迟加载
(4)序列化和反序列化
(5)反射攻击

2. 懒汉模式

在这里插入图片描述
多线程创建:
在这里插入图片描述
主函数直接调用
在这里插入图片描述
开启线程调试:类型设置为Thread
在这里插入图片描述
开始调试:
在这里插入图片描述
thread0
在这里插入图片描述
thread1
在这里插入图片描述
直接往下走,生成两个对象。
在这里插入图片描述

3. 双重检验锁

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;

    private LazyDoubleCheckSingleton(){

    }

    public static LazyDoubleCheckSingleton getInstance(){
        if(lazySingleton==null){//1.第一次检查,此处是为了减少性能开销,不用每次进入synchronized中
            synchronized (LazyDoubleCheckSingleton.class) {
                if(lazySingleton==null){//2.第二次检测

                    /*问题:原来的排序过程是:1、2、3,但是可能jvm指令重排序成为1、3、2,
                    * 此时可能两个线程都在执行2,执行3的时候这两个线程都new了LazyDoubleCheckSingleton对象
                    *
                    * 解决办法:volatile
                    * */
                    //1.分配内存给这个对象
                    //3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
                    //2.new一个LazyDoubleCheckSingleton对象
                    lazySingleton = new LazyDoubleCheckSingleton();
                }
            }
        }

        return lazySingleton;
    }

}

4. 静态内部类

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton(){
    }
}

静态内部类在外部类被调用时不会立即被初始化,只有当其中的静态内部参数被调用时,才会被初始化。这样保证懒加载。
通过静态内部类的初始化staticInnerClassSingleton,jvm保证类的初始化只能有一个线程可以同时初始化同一个类,也就是获取初始化InnerClass静态类的锁只会被一个线程获取到,所以在InnerClass被初始化时,其中的静态资源会优先被初始化。

相比双重检验锁:毕竟volatile禁止了指令重排,所以双重检验锁的效率肯定略低一点。

5.懒汉模式

public class HungrySingleton implements Serializable {
    private static HungrySingleton ourInstance = new HungrySingleton();

    public static HungrySingleton getInstance() {
        return ourInstance;
    }

    private HungrySingleton() {
    }
}

特点:在类加载的时候完成对单例类的初始化。

6.序列化对单例的破坏

在这里插入图片描述
流程
(1)ObjectInputStream中readObject读取一个object
(2)ObjectInputStream中readObject0读取
(3)readObject0方法中发现是一个object调用readOrdinaryObject

case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

(4)readOrdinaryObject中有,表示只要是实现了序列化就new一个对象,否则返回null

obj = desc.isInstantiable() ? desc.newInstance() : null;

(5)调用了cons.newInstance()

@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

(6)Reflection.getCallerClass()调用反射实现了new一个新的对象。

故前后不是同一个对象。

重写:
(1)在readOrdinaryObject中反射完成一个对象的创建以后会调用desc.invokeReadResolve(obj)
(2)desc.invokeReadResolve(obj)中return readResolveMethod.invoke(obj, (Object[]) null);
(3) readResolveMethod.invoke调用原方法的readResolve方法,于是可以通过重写readResolve实现返回原来的单例对象。

在这里插入图片描述

7. 反射攻击单例

反射修改私有构造器的权限来new一个新的单例对象。
在这里插入图片描述
防止反射攻击
在这里插入图片描述
懒汉式在多线程条件下无法防止反射在这里插入图片描述
无法防止的原因:先利用构造器new了一个单例对象,然后getInstance()方法new了一个对象。
在这里插入图片描述

8. 枚举完成单例

枚举类型
在这里插入图片描述
反序列化无法攻击枚举
在这里插入图片描述
在ObjectInputStream中readEnum方法去读取数据时,会从常量池中去寻找这些数据。所以枚举中的data无论何时被获取都是相等。

反射攻击枚举:报错,获取构造器失败

查看源码发现,枚举没有空构造器,必须要带上参数

在这里插入图片描述
传入参数后重试
在这里插入图片描述
查看源码:发现构造器无法获取枚举对象
在这里插入图片描述

继续使用jad EnumInstance.class生成EnumInstance.jad,里面有如下代码:

public final class EnumInstance extends Enum // final类
{

    public static EnumInstance[] values()
    {
        return (EnumInstance[])$VALUES.clone();
    }

    public static EnumInstance valueOf(String name)
    {
        return (EnumInstance)Enum.valueOf(com/qianliu/creational/songleton/EnumInstance, name);
    }

    private EnumInstance(String s, int i)//私有构造器
    {
        super(s, i);
    }

    public static EnumInstance getInstance()
    {
        return INSTANCE;
    }

    public String getData()
    {
        return data;
    }

    public void setData(String data)
    {
        this.data = data;
    }

    public static final EnumInstance INSTANCE;
    String data;
    private static final EnumInstance $VALUES[];

    static 
    {
        INSTANCE = new EnumInstance("INSTANCE", 0);//在静态模块初始化单例对象
        $VALUES = (new EnumInstance[] {
            INSTANCE
        });
    }
}

Enum类似于饿汉模式声明对象,类加载时完成初始化,再加上反射和io相关的类保证枚举类型的单例。

使用枚举类内部的方法:枚举类内部的方法必须显式声明。
在这里插入图片描述

9. 容器实现单例

如果我们使用的过程中单例对象特别多,可以用这种方式实现单例
在这里插入图片描述

10. ThreadLocal实现特殊的单例

在这里插入图片描述
ThreadLocal可以保证每个线程内部的对象是单例的,不同线程直接按互不干扰
在这里插入图片描述

源码:https://github.com/LUK-qianliu/design_pattern_in_all

发布了275 篇原创文章 · 获赞 42 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/qq_35688140/article/details/102523932