美文网首页
急了急了,破防单例模式

急了急了,破防单例模式

作者: 晏子小七 | 来源:发表于2020-11-04 18:04 被阅读0次

    本文主要介绍单例创建的集中方式和反射给单例造成的影响。

    单例的定义

    单例模式:保证一个类仅有一个实例对象,并且提供一个全局访问点。

    单例的特点

    • 单例类只能有一个实例对象
    • 单例类必须自己创建自己的唯一实例
    • 单例类必须对外提供一个访问该实例的方法

    使用场景及优点

    优:

    • 提供了对唯一实例的受控访问
    • 保证了内存中只有唯一实例,减少内存开销,比如需要多次创建和销毁实例的场景
    • 避免对资源的多重占用,比如文件的写操作

    缺:

    • 没有抽象层,接口,不能继承,扩展困难,违反了开闭原则
    • 单例类一般写在同一个类中,职责过重,违背了单一职责原则

    应用场景:

    文件系统;数据库连接池的设计;日志系统等 IO/生成唯一序列号/身份证/对象需要共享的情况,比如web中配置对象

    实现单例

    三步:

    1. 构造函数私有化
    2. 在类内部创建实例
    3. 提供本类实例的唯一全局访问点,即唯一实例的方法
    饿汉式:
    public class Hungry {
        // 构造器私有,静止外部new
        private Hungry(){}
    
        // 在类的内部创建自己的实例
        private static Hungry hungry = new Hungry();
    
        // 获取本类实例的唯一全局访问点
        public static Hungry getHungry(){
            return hungry;
        }
    }
    
    
    懒汉式:
    public class Lazy1 {
        // 构造器私有,静止外部new
        private Lazy1(){
            System.out.println(Thread.currentThread().getName() + " 访问到了");
        }
    
        // 定义即可,不真正创建
        private static Lazy1 lazy1 = null;
    
        // 获取本类实例的唯一全局访问点
        public static Lazy1 getLazy1(){
            // 如果实例不存在则new一个新的实例,否则返回现有的实例
            if (lazy1 == null) {
                lazy1 = new Lazy1();
            }
            return lazy1;
        }
    
        public static void main(String[] args) {
            // 多线程访问,看看会有什么问题
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    Lazy1.getLazy1();
                }).start();
            }
        }
    }
    
    

    单线程环境下是没有问题的,但是多线程的情况下就会出现问题

    DCL 懒汉式:

    方法上直接加锁:

    public static synchronized Lazy1 getLazy1(){
        if (lazy1 == null) {
            lazy1 = new Lazy1();
        }
        return lazy1;
    }
    
    

    缩小锁范围:

    public static Lazy1 getLazy1(){
        if (lazy1 == null) {
            synchronized(Lazy1.class){
                lazy1 = new Lazy1();
            }
        }
        return lazy1;
    }
    
    

    双重锁定:

    // 获取本类实例的唯一全局访问点
    public static Lazy1 getLazy1(){
        // 如果实例不存在则new一个新的实例,否则返回现有的实例
        if (lazy1 == null) {
            // 加锁
            synchronized(Lazy1.class){
                // 第二次判断是否为null
                if (lazy1 == null){
                    lazy1 = new Lazy1();
                }
            }
        }
        return lazy1;
    }
    
    

    指令重排序: 指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。

    首先要知道 lazy1 = new Lazy1(); 这一步并不是一个原子性操作,也就是说这个操作会分成很多步

    ① 分配对象的内存空间 ② 执行构造函数,初始化对象 ③ 指向对象到刚分配的内存空间

    但是 JVM 为了效率对这个步骤进行了重排序,例如这样:

    ① 分配对象的内存空间 ③ 指向对象到刚分配的内存空间,对象还没被初始化 ② 执行构造函数,初始化对象

    解决的方法很简单——在定义时增加 volatile 关键字,避免指令重排

    最终代码:

    public class Lazy1 {
        // 构造器私有,静止外部new
        private Lazy1(){
            System.out.println(Thread.currentThread().getName() + " 访问到了");
        }
    
        // 定义即可,不真正创建
        private static volatile Lazy1 lazy1 = null;
    
        // 获取本类实例的唯一全局访问点
        public static Lazy1 getLazy1(){
            // 如果实例不存在则new一个新的实例,否则返回现有的实例
            if (lazy1 == null) {
                // 加锁
                synchronized(Lazy1.class){
                    // 第二次判断是否为null
                    if (lazy1 == null){
                        lazy1 = new Lazy1();
                    }
                }
            }
            return lazy1;
        }
    
        public static void main(String[] args) {
            // 多线程访问,看看会有什么问题
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    Lazy1.getLazy1();
                }).start();
            }
        }
    }
    
    

    静态内部类懒汉式单例:

    双重锁定算是一种可行不错的方式,而静态内部类就是一种更加好的方法,不仅速度较快,还保证了线程安全,先看代码:

    public class Lazy2 {
        // 构造器私有,静止外部new
        private Lazy2(){
            System.out.println(Thread.currentThread().getName() + " 访问到了");
        }
    
        // 用来获取对象
        public static Lazy2 getLazy2(){
            return InnerClass.lazy2;
        }
    
        // 创建内部类
        public static class InnerClass {
            // 创建单例对象
            private static Lazy2 lazy2 = new Lazy2();
        }
    
        public static void main(String[] args) {
            // 多线程访问,看看会有什么问题
            for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    Lazy2.getLazy2();
                }).start();
            }
        }
    }
    
    

    上面的代码,首先 InnerClass 是一个内部类,其在初始化时是不会被加载的,当用户执行了 getLazy2() 方法才会加载,同时创建单例对象,所以他也是懒汉式的方法,因为 InnerClass 是一个静态内部类,所以只会被实例化一次,从而达到线程安全,因为并没有加锁,所以性能上也会很快。

    枚举创建单例:

    public enum EnumSingle {
        IDEAL;
    }
    
    

    代码就这样,简直不要太简单,访问通过 EnumSingle.IDEAL 就可以访问了


    反射破坏单例模式

    单例是如何被破坏的:

    这是我们原来的写法,new 两个实例出来,输出一下

    public class Lazy1 {
        // 构造器私有,静止外部new
        private Lazy1(){
            System.out.println(Thread.currentThread().getName() + " 访问到了");
        }
    
        // 定义即可,不真正创建
        private static volatile Lazy1 lazy1 = null;
    
        // 获取本类实例的唯一全局访问点
        public static Lazy1 getLazy1(){
            // 如果实例不存在则new一个新的实例,否则返回现有的实例
            if (lazy1 == null) {
                // 加锁
                synchronized(Lazy1.class){
                    // 第二次判断是否为null
                    if (lazy1 == null){
                        lazy1 = new Lazy1();
                    }
                }
            }
            return lazy1;
        }
    
        public static void main(String[] args) {
    
            Lazy1 lazy1 = getLazy1();
            Lazy1 lazy2 = getLazy1();
            System.out.println(lazy1);
            System.out.println(lazy2);
    
        }
    }
    
    

    运行结果: main 访问到了 cn.ideal.single.Lazy1@1b6d3586 cn.ideal.single.Lazy1@1b6d3586

    可以看到,结果是单例没有问题

    一个普通实例化,一个反射实例化:
    public static void main(String[] args) throws Exception {
        Lazy1 lazy1 = getLazy1();
        // 获得其空参构造器
        Constructor<Lazy1>  declaredConstructor = Lazy1.class.getDeclaredConstructor(null);
        // 使得可操作性该 declaredConstructor 对象
        declaredConstructor.setAccessible(true);
        // 反射实例化
        Lazy1 lazy2 = declaredConstructor.newInstance();
        System.out.println(lazy1);
        System.out.println(lazy2);
    }
    
    

    运行结果:

    main 访问到了 main 访问到了 cn.ideal.single.Lazy1@1b6d3586 cn.ideal.single.Lazy1@4554617c

    可以看到,单例被破坏了

    如何解决:因为我们反射走的其无参构造,所以在无参构造中再次进行非null判断,加上原来的双重锁定,现在也就有三次判断了。
    解决方案:增加一个标识位,例如下文通过增加一个布尔类型的 ideal 标识,保证只会执行一次,更安全的做法,可以进行加密处理,保证其安全性。

    这样就没问题了吗,并不是,一旦别人通过一些手段得到了这个标识内容,那么他就可以通过修改这个标识继续破坏单例,代码如下(这个把代码贴全一点,前面都是节选关键的,都可以参考这个)

    public class Lazy1 {
    
        private static boolean ideal = false;
    
        // 构造器私有,静止外部new
        private Lazy1(){
            synchronized (Lazy1.class){
                if (ideal == false){
                    ideal = true;
                } else {
                    throw new RuntimeException("反射破坏单例异常");
                }
            }
            System.out.println(Thread.currentThread().getName() + " 访问到了");
        }
    
        // 定义即可,不真正创建
        private static volatile Lazy1 lazy1 = null;
    
        // 获取本类实例的唯一全局访问点
        public static Lazy1 getLazy1(){
            // 如果实例不存在则new一个新的实例,否则返回现有的实例
            if (lazy1 == null) {
                // 加锁
                synchronized(Lazy1.class){
                    // 第二次判断是否为null
                    if (lazy1 == null){
                        lazy1 = new Lazy1();
                    }
                }
            }
            return lazy1;
        }
    
        public static void main(String[] args) throws Exception {
    
            Field ideal = Lazy1.class.getDeclaredField("ideal");
            ideal.setAccessible(true);
    
            // 获得其空参构造器
            Constructor<Lazy1> declaredConstructor = Lazy1.class.getDeclaredConstructor(null);
            // 使得可操作性该 declaredConstructor 对象
            declaredConstructor.setAccessible(true);
            // 反射实例化
            Lazy1 lazy1 = declaredConstructor.newInstance();
            ideal.set(lazy1,false);
            Lazy1 lazy2 = declaredConstructor.newInstance();
    
            System.out.println(lazy1);
            System.out.println(lazy2);
    
        }
    }
    
    

    运行结果: main 访问到了 main 访问到了 cn.ideal.single.Lazy1@4554617c cn.ideal.single.Lazy1@74a14482 实例化 lazy1 后,其执行了修改 ideal 这个布尔值为 false,从而绕过了判断,再次破坏了单例 所以,可以得出,这几种方式都是不安全的,都有着被反射破坏的风险。

    相关文章

      网友评论

          本文标题:急了急了,破防单例模式

          本文链接:https://www.haomeiwen.com/subject/fvogvktx.html