关于设计模式:单例模式的几种实现And反射对其的破坏

1次阅读

共计 9659 个字符,预计需要花费 25 分钟才能阅读完成。

一 单例模式概述

(一) 什么是单例模式

单例模式属于 创立型模式 之一,它提供了一种创建对象的最佳形式

在软件工程中,创立型模式是解决对象创立的设计模式,试图依据理论状况应用适合的形式创建对象。根本的对象创立形式可能会导致设计上的问题,或减少设计的复杂度。创立型模式通过以某种形式管制对象的创立来解决问题。

因为咱们平时尽管能够定义一个全局变量使一个对象被拜访,然而它并不能保障你屡次实例化对象,最直观的,屡次创建对象的代价就是耗费性能,导致效率会低一些。单例模式就是用来解决这些问题

顺便提一个很常见的例子:例如在 Win 系的电脑下咱们永远只能关上一个工作管理器,这样能够避免出现一些资源节约,以及多窗口显示数据不统一的问题

定义:单例模式,保障一个类仅有一个实例,并且提供一个拜访它的全局拜访点

(二) 特点

  • ① 单例类只能有一个实例对象
  • ② 单例类必须本人创立本人的惟一实例
  • ③ 单例类必须对外提供一个拜访该实例的办法

(三) 优缺点以及应用场景

(1) 长处

  • 提供了对惟一实例的受控拜访
  • 保障了内存中只有惟一实例,缩小了内存的开销

    • 尤其体现在一些须要屡次创立销毁实例的状况下
  • 防止对资源的多重占用

    • 比方对文件的写操作

(2) 毛病

  • 单例模式中没有形象层,没有接口,不能继承,扩大艰难,扩大须要批改原来的代码,违反了 “开闭准则”
  • 单例类的代码个别写在同一个类中,肯定水平上职责过重,违反了 “繁多职责准则”

(3) 利用场景

先说几个大家常见单例的例子:

  • Windows 下的工作管理器和回收站,都是典型的单例模式,你能够试一下,没法同时关上两个的哈
  • 数据库连接池的设计个别也是单例模式,因为频繁的关上敞开与数据库的连贯,会有不小的效率损耗

    • 然而滥用单例也可能带来一些问题,例如导致共享连接池对象的程序过多而呈现连接池溢出
  • 网站计数器,通过单例解决同步问题
  • 操作系统的文件系统
  • Web 利用的配置对象读取,因为配置文件属于共享的资源
  • 程序的日志利用,个别也是单例,否则追加内容时,容易出问题

所以,依据一些常见的例子,简略总结一下,什么时候用单例模式呢?

  • ① 须要频繁创立销毁实例的
  • ② 实例创立时,耗费资源过多,或者耗时较多的,例如数据连贯或者 IO
  • ③ 某个类只要求生成一个类的状况,例如生成惟一序列号,或者人的身份证
  • ④ 对象须要共享的状况,如 Web 中配置对象

二 实现单例模式

依据单例模式的定义和特点,咱们能够分为三步来实现最根本的单例模式

  • ① 构造函数私有化
  • ② 在类的外部创立实例
  • ③ 提供本类实例的惟一全局拜访点,即提供获取惟一实例的办法

(一) 饿汉式

咱们就依照最根本的这三点来写

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();}
    }
}

例如上述代码,咱们只在刚开始做了一个定义,真正的实例化是在调用 getLazy1() 时被执行

单线程环境下是没有问题的,然而多线程的状况下就会呈现问题,例如上面是我运行后果中的一次:

Thread-0 拜访到了
Thread-4 拜访到了
Thread-1 拜访到了
Thread-3 拜访到了
Thread-2 拜访到了

(三) DCL 懒汉式

(1) 办法上间接加锁

很显然,多线程下的一般懒汉式呈现了问题,这个时候,咱们只须要加一层锁就能够解决

简略的做法就是在办法前加上 synchronized 关键字

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

(2) 放大锁的范畴

然而咱们又想放大锁的范畴,毕竟办法上加锁,多线程中效率会低一些,所以只把锁加到须要的代码上

咱们直观的可能会这样写

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

然而这样还是有问题的

(3) 双重锁定

当线程 A 和 B 同时拜访 getLazy1(),执行到到 if (lazy1 == null) 这句的时候,同时判断出 lazy1 == null,也就同时进入了 if 代码块中,前面因为加了锁,只有一个能先执行实例化的操作,例如 A 先进入,然而 前面的 B 进入后同样也能够创立新的实例,就达不到单例的目标了,不信能够本人试一下

解决的形式就是再进行第二次的判断

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

(4) 指令重排问题

这种在适当地位加锁的形式,尽可能的升高了加锁对于性能的影响,也能达到预期成果

然而这段代码,在肯定条件下还是会有问题,那就是指令重排问题

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

什么意思呢?

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

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

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

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

依照 ① ③ ② 的程序,当 A 线程执行到 ② 后,B 线程判断 lazy1 != null,然而此时的 lazy1 还没有被初始化,所以会出问题,并且这个过程中 B 基本执行到锁那里,配个表格阐明一下:

Time ThreadA ThreadB
t1 A:① 调配对象的内存空间
t2 A:③ 指向对象到刚调配的内存空间,对象还没被初始化
t3 B:判断 lazy1 是否为 null
t4 B:判断到 lazy1 != null,返回了一个没被初始化的对象
t5 A:② 初始化对象

解决的办法很简略——在定义时减少 volatile 关键字,防止指令重排

(5) 最终代码

最终代码如下:

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 是一个动态外部类,所以只会被实例化一次,从而达到线程平安,因为并没有加锁,所以性能上也会很快,所以个别是举荐的

(五) 枚举形式

最初举荐一个十分好的形式,那就是枚举单例形式,其不仅简略,且保障了平安,先看一下《Effective Java》中作者的阐明:

这种办法在性能上与私有域办法类似,但更加简洁无偿地提供了序列化机制,相对避免屡次实例化。即便是在面对简单的序列化或者反射攻打的时候。尽管这种办法还没有宽泛采纳,然而单元素的枚举类型常常成为实现 Singleton 的最佳办法,留神,如果 Singleton 必须扩大一个超类,而不是扩大 enum 时则不宜应用这个办法,(尽管能够申明枚举去实现接口)。

节选自《Effective Java》第 3 条:用公有结构器或者枚举类型强化 Singleton 属性

原著:Item3: Enforce the singleton property with a private constructor or an enum

代码就这样,几乎不要太简略,拜访通过 EnumSingle.IDEAL 就能够拜访了

public enum 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

能够看到,后果是单例没有问题

(1) 一个一般实例化,一个反射实例化

然而咱们如果通过反射的形式进行实例化类,会有什么问题呢?

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);
}

getDeclaredConstructor() 办法阐明

办法返回一个 Constructor 对象,它反映此 Class 对象所示意的类或接口指定的构造函数。parameterTypesparameter 是确定构造函数的形参类型,在 Class 对象申明程序的数组。

public Constructor<T> getDeclaredConstructor(Class<?>… parameterTypes) throws NoSuchMethodException, SecurityException

运行后果:

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

能够看到,单例被毁坏了

解决办法:因为咱们反射走的其无参结构,所以在无参结构中再次进行非 null 判断,加上原来的双重锁定,当初也就有三次判断了

// 结构器公有,静止内部 new
private Lazy1(){synchronized (Lazy1.class){if(lazy1 != null) {throw new RuntimeException("反射毁坏单例异样");
        } 
    }
}

不过后果也没让人悲观,这种测试下,第二次实例化会间接报异样

(2) 两个都是反射实例化

如果两个都是反射实例化进去的,也就是说,基本就不去调用 getLazy1() 办法,那可怎么办?

如下:

public static void main(String[] args) throws Exception {

    // 取得其空参结构器
    Constructor<Lazy1> declaredConstructor = Lazy1.class.getDeclaredConstructor(null);
    // 使得可操作性该 declaredConstructor 对象
    declaredConstructor.setAccessible(true);
     // 反射实例化
    Lazy1 lazy1 = declaredConstructor.newInstance();
    Lazy1 lazy2 = declaredConstructor.newInstance();

    System.out.println(lazy1);
    System.out.println(lazy2);
}

运行后果:

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

单例又被毁坏了

解决方案:减少一个标识位,例如下文通过减少一个布尔类型的 ideal 标识,保障只会执行一次,更平安的做法,能够进行加密解决,保障其安全性

// 结构器公有,静止内部 new
private Lazy1(){synchronized (Lazy1.class){if (ideal == false){ideal = true;} else {throw new RuntimeException("反射毁坏单例异样");
        }
    }
    System.out.println(Thread.currentThread().getName() + "拜访到了");
}

这样就没问题了吗,并不是,一旦他人通过一些伎俩失去了这个标识内容,那么他就能够通过批改这个标识持续毁坏单例,代码如下(这个把代码贴全一点,后面都是节选要害的,都能够参考这个)

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,从而绕过了判断,再次毁坏了单例

所以,能够得出,这几种形式都是不平安的,都有着被反射毁坏的危险

(二) 枚举类不会被毁坏

下面在解说枚举单例形式的时候就提过《Effective Java》中提到,即便是在面对简单的序列化或者反射攻打的时候,(枚举单例形式)相对避免屡次实例化,上面来看一下是不是这样:

首先说一个前提条件:这是 Constructor 下的 newInstance 办法节选,也就是说遇到枚举时,会报异样,也就是不容许通过反射创立枚举

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");

看一下咱们枚举单例类 EnumSingle 生成的字节码文件,能够看到其中有一个无参结构,也就是说,咱们还是只须要拿到 getDeclaredConstructor(null) 就行了

代码如下:

public enum EnumSingle {
    IDEAL;

    public static void main(String[] args) throws Exception {
        EnumSingle ideal1 = EnumSingle.IDEAL;
        // 取得其空参结构器
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        // 使得可操作性该 declaredConstructor 对象
        declaredConstructor.setAccessible(true);
        // 反射实例化
        EnumSingle ideal2 = declaredConstructor.newInstance();
        System.out.println(ideal1);
        System.out.println(ideal2);
    }
}

运行后果却是出乎意料:

提醒居然是找不到这个空参???字节码中可是却是存在的啊

Exception in thread "main" java.lang.NoSuchMethodException: cn.ideal.single.EnumSingle.<init>()

本人 javap 反编译一下,能够看到还是有这个空参

换成 jad 再看看(将 jad.exe 放在字节码文件同目录下)

  • 执行:jad -sjava EnumSingle.class

提醒曾经反编译完结:Parsing EnumSingle.class… Generating EnumSingle.java

关上生成的 java 文件,终于发现,原来它是一个带参结构,同时有两个参数,String 和 int

所以上面,咱们只须要批改原来的无参为有参即可:

public enum EnumSingle {
    IDEAL;

    public static void main(String[] args) throws Exception {
        EnumSingle ideal1 = EnumSingle.IDEAL;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        // 使得可操作性该 declaredConstructor 对象
        declaredConstructor.setAccessible(true);
        // 反射实例化
        EnumSingle ideal2 = declaredConstructor.newInstance();
        System.out.println(ideal1);
        System.out.println(ideal2);
    }
}

这样就没问题了,提醒了咱们想要的谬误:Cannot reflectively create enum objects

这也阐明,枚举类的单例模式写法的确不会被反射毁坏!

正文完
 0