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

急了急了,破防单例模式

作者: 晏子小七 | 来源:发表于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,从而绕过了判断,再次破坏了单例 所以,可以得出,这几种方式都是不安全的,都有着被反射破坏的风险。

相关文章

  • 急了急了,破防单例模式

    本文主要介绍单例创建的集中方式和反射给单例造成的影响。 [#%E5%8D%95%E4%BE%8B%E7%9A%84...

  • 她急了,他急了

    继续为回家做准备。 昨天下午,打探了旁边医院做核酸的时间和方式,又去花卉市场给老爸买了几样简单的花(杜鹃、长寿花、...

  • 急了

    这么多资源,聊了半天就十来个人搭理我,你们咋就那么淡定呢?

  • 急了

    很高兴找到了一个让自己有兴致的东西,兴趣可以引导你去了解一个事物,能让你有持续的热情的就要靠耐心了,如果一开始透支...

  • 急了

    昨天接到电话通知,批文下来了。落实一下什么时候可以订机票。 “随时可以,明天走吗” “哪有那么快,先了解一下” “...

  • 【设计模式】单例模式

    单例模式 常用单例模式: 懒汉单例模式: 静态内部类单例模式: Android Application 中使用单例模式:

  • Android设计模式总结

    单例模式:饿汉单例模式://饿汉单例模式 懒汉单例模式: Double CheckLock(DCL)实现单例 Bu...

  • 太极

    挥手,高过月 太急了,太急了 成了太极——拳 又一拳 太急了,太急了 成了太极——剑

  • 2018-04-08php实战设计模式

    一、单例模式 单例模式是最经典的设计模式之一,到底什么是单例?单例模式适用场景是什么?单例模式如何设计?php中单...

  • 设计模式之单例模式详解

    设计模式之单例模式详解 单例模式写法大全,也许有你不知道的写法 导航 引言 什么是单例? 单例模式作用 单例模式的...

网友评论

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

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