美文网首页
创建型模式——单例模式(四)

创建型模式——单例模式(四)

作者: 最后的轻语_dd43 | 来源:发表于2019-05-20 08:40 被阅读0次

    该项目源码地址:https://github.com/ggb2312/JavaNotes/tree/master/design-pattern(设计模式相关代码与笔记)

    1. 定义

    保证一个类仅有一个实例,并提供一个全局访问点

    2. 介绍

    适用场景
    想确保任何情况下都绝对只有一个实例

    单例模式的重点

    • 私有构造器
    • 线程安全
    • 延迟加载
    • 序列化和反序列化安全
    • 反射攻击

    3. 模式实例

    在Java中,我们通过使用对象(类实例化后)来操作这些类,类实例化是通过它的构造方法进行的,要是想实现一个类只有一个实例化对象,就要对类的构造方法下功夫。

    类图

    单例模式的一般实现:(含使用步骤)

    public class Singleton {
    //1. 创建私有变量 ourInstance(用以记录 Singleton 的唯一实例)
    //2. 内部进行实例化
        private static Singleton ourInstance  = new  Singleton();
    
    //3. 把类的构造方法私有化,不让外部调用构造方法实例化
        private Singleton() {
        }
    //4. 定义公有方法提供该类的全局唯一访问点
    //5. 外部通过调用getInstance()方法来返回唯一的实例
        public static  Singleton newInstance() {
            return ourInstance;
        }
    }
    

    3.1 懒汉式(延迟加载)

    懒汉式基础实现

    特点:懒加载,需要时才创建,线程不安全

    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
        private LazySingleton(){
    
        }
        public static LazySingleton getInstance(){
            if(lazySingleton == null){
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    }
    

    测试

    public class Test {
        public static void main(String[] args){
            LazySingleton lazySingleton1 = LazySingleton.getInstance();
            LazySingleton lazySingleton2 = LazySingleton.getInstance();
            System.out.println(lazySingleton1);
            System.out.println(lazySingleton1);
            System.out.println(lazySingleton1 == lazySingleton2);
        }
    }
    
    测试结果

    懒汉式是线程不安全的,假如有两个线程使用懒汉式创建对象,thread1调用getInstance()方法时,lazySingleton == null为true,进入if,但未new对象。此时cpu调度,thread2调用getInstance()方法时,lazySingleton == null为true,进入if,并new了对象,返回给thread2。此时thread1开始在if里面new对象,返回给thread1.创建了两次对象。

    懒汉式多线程创建对象测试

    public class T implements Runnable {
        @Override
        public void run() {
            LazySingleton lazySingleton = LazySingleton.getInstance();
            System.out.println(Thread.currentThread().getName()+"  " + lazySingleton);
        }
    }
    

    修改测试类

    public class Test {
        public static void main(String[] args){
            Thread t1 = new Thread(new T());
            Thread t2 = new Thread(new T());
            t1.start();
            t2.start();
            System.out.println("end");
        }
    }
    

    在多线程debug,人为干扰的情况下(或者多run几次也可以),创建了两个不同的对象。

    测试结果

    3.1.1 同步锁

    特点:使用同步锁,线程安全,但性能比较差
    修改LazySingleton单例类(静态方法synchronized会锁住这个文件)

    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
        private LazySingleton(){
    
        }
        public synchronized static LazySingleton getInstance(){
            if(lazySingleton == null){
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    }
    

    在多线程debug,人为干扰的情况下,同步锁会保证只有一个线程进入同步方法,创建对象。

    3.1.2 double-checked locking(双重检查加锁)

    特点:懒加载,jdk1.5及以上版本线程安全,性能好
    创建LazyDoubleCheckSingleton类

    public class LazyDoubleCheckSingleton {
        private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
        private LazyDoubleCheckSingleton(){
    
        }
        public static LazyDoubleCheckSingleton getInstance(){
            if(lazyDoubleCheckSingleton == null){
                synchronized (LazyDoubleCheckSingleton.class){
                    if(lazyDoubleCheckSingleton == null){
                        lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    }
                }
            }
            return lazyDoubleCheckSingleton;
        }
    }
    

    在代码的第12行首先判断lazyDoubleCheckSingleton是否为null(是否分配内存地址),如果lazyDoubleCheckSingleton为null使用synchronized同步锁保证线程安全,将同步锁放在if判断内比直接放在方法上,大大减少了性能开销。

    我们来模拟一下多线程情况下。

    thread1与thread2都进入了12行iflazyDoubleCheckSingleton == null判断为true,进入if。thread1握住了锁进入了同步代码块,thread2阻塞。thread1进入14行再次iflazyDoubleCheckSingleton == null判断为true,进入15行new对象,释放同步锁,return对象。thread2握住了锁进入了同步代码块,iflazyDoubleCheckSingleton == null判断为false,释放锁,直接return对象。

    看似没有任何问题,实际上会出现问题的,问题出在第12行和第15行,分析如下:

    我们通常会将第15行lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();看成是一个步骤,实际上JVM内部已经转换为三条指令。

    三条指令如下:

    步骤一: memory = allocate();——》分配对象的内存空间
    步骤二: ctorInstance(memory);——》初始化对象
    步骤三: instance = memory;——》设置lazyDoubleCheckSingleton 指向刚分配的内存地址

    对象创建图示:

    java对象创建过程

    在这里会出现一个指令重排的问题。

    指令重排:大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。
    除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。

    经过重排序后的对象创建过程如下:

    步骤一: memory = allocate();——》分配对象的内存空间
    步骤三: instance = memory; ——》设置lazyDoubleCheckSingleton 指向刚分配的内存地址
    步骤二: ctorInstance(memory);——》初始化对象

    经过重排序后的对象创建过程图示如下:

    指令重排后的对象创建

    在单线程指令重排的情况下,由于“intra-thread semantics”的存在,保证指令重排序不会改变单线程内的程序执行结果。

    在多线程指令重排的情况下,thread1进入了12行lazyDoubleCheckSingleton == null判断为true,进入if。thread1握住了锁进入了同步代码块。thread1进入14行再次lazyDoubleCheckSingleton == null判断为true,进入15行new对象,在new对象的过程中:1.分配对象的内存空间 3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址。此时thread2调用getInstance(),进入了12行lazyDoubleCheckSingleton == null判断为false(ps:java的“==”比的内存地址,此时lazyDoubleCheckSingleton已经分配内存地址了),直接返回现有的对象lazyDoubleCheckSingleton,thread2使用lazyDoubleCheckSingleton时就会出错,抛异常,因为lazyDoubleCheckSingleton并未被初始化。

    多线程指令重排

    上面说的那么多,大家估计会晕,总结一下原因:thread1在第15行执行“1.分配对象的内存空间地址、3.设置instance指向内存空间地址”时,thread2在第12行判断instance是否为null,由于thread1设置了instance的内存空间地址,所以返回false,直接返回instance,thread2就会直接拿着instance去使用,instance没有被初始化就会报错。

    归根究底,是因为thread1指令重排过程,thread2使用了未初始化的对象。

    我们知道了问题所在,就可以从两方面入手。
    方法1.不允许thread1第二步与第三步指令重排。
    方法2.thread1指令重排时,不让thread2看到这个指令重排。

    使用volatile关键字是使用方法1 不允许thread1第二步与第三步指令重排。

    关于volatile:
    在多线程情况下,cpu会有共享内存,在加入volatile关键字后,所有线程都可以看到共享内存的最新状态,保证内存的可见性。
    用volatile关键字修饰的共享变量,在进行写操作时,会多出一些汇编代码,主要作用:会将当前处理器的缓存行的数据写到系统内存中,这个写回内存的操作,会使其他处理器缓存的数据失效,由于处理器缓存的数据失效了,它们就会从共享内存同步数据,这样就保证了内存的可见性(缓存一致性协议)。

    使用volatile关键字重写LazyDoubleCheckSingleton类

    package com.desgin.pattern.creational.singleton;
    /**
     * Create by lastwhisper on 2019/1/25
     */
    public class LazyDoubleCheckSingleton {
        private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
        private LazyDoubleCheckSingleton(){
    
        }
        public static LazyDoubleCheckSingleton getInstance(){
            if(lazyDoubleCheckSingleton == null){
                synchronized (LazyDoubleCheckSingleton.class){
                    if(lazyDoubleCheckSingleton == null){
                        lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                    }
                }
            }
            return lazyDoubleCheckSingleton;
        }
    }
    

    修改多线程T类

    package com.desgin.pattern.creational.singleton;
    
    /**
     * Create by lastwhisper on 2019/1/25
     */
    public class T implements Runnable {
        @Override
        public void run() {
            LazyDoubleCheckSingleton lzyDoubleCheckSingleton = LazyDoubleCheckSingleton.getInstance();
            System.out.println(Thread.currentThread().getName()+"  " + lzyDoubleCheckSingleton);
        }
    }
    

    测试类

    package com.desgin.pattern.creational.singleton;
    /**
     * Create by lastwhisper on 2019/1/25
     */
    public class Test {
        public static void main(String[] args){
                Thread t1 = new Thread(new T());
                Thread t2 = new Thread(new T());
                t1.start();
                t2.start();
                System.out.println("end");
        }
    }
    

    测试结果:

    测试结果

    3.1.3 静态内部类

    特点:不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
    使用静态内部类是使用方法2:thread1指令重排时,不让thread2看到这个指令重排(ps:因为jvm会使用初始化锁保证多个线程下只会有一个线程加载类)。

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

    那么,静态内部类又是如何实现线程安全的呢?
    首先,我们先了解下类的加载时机。有5种情况,首次发生时,一个类将被立刻初始化,类是泛指,包括接口。

    1.有一个类的实例被创建
    2.类中声明的静态方法被调用
    3.类中声明的静态成员被赋值
    4.类中声明的静态成员被使用,且不是常量成员
    5.类是顶级类,且类中有嵌套的断言语句

    我们这里使用的是4.类中声明的静态成员被使用,且不是常量成员。

    JVM在类的初始化阶段(也就是class被加载后,被线程使用前,都是类的初始化阶段),JVM会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
    ps:<clinit>()是用于初始化静态的类变量, <init>()是初始化实例变量

    jvm初始化锁图示

    简单来说:在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化

    修改多线程T类

    public class T implements Runnable {
        @Override
        public void run() {
            StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
           System.out.println(Thread.currentThread().getName() + "  " + instance);
        }
    }
    

    测试结果:

    测试结果

    3.2 饿汉式(立即加载)

    饿汉式

    特点:实现简单,由于是立即加载,如果这个类一直不被使用就会浪费内存。

    public class HungrySingleton {
        private static HungrySingleton hungrySingleton;
        static {
            hungrySingleton = new HungrySingleton();
        }
        private HungrySingleton() {
    
        }
        public static HungrySingleton getInstance() {
            return hungrySingleton;
        }
    }
    

    简单来说,在类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化,保证线程安全(ps:详细解释在3.1.3 静态内部类)

    4. 序列化破坏解决方案及原理分析

    使用上述的任意一个正确的单例模式进行序列化破坏测试都可以,这里我们选择饿汉式进行测试。

    4.1 序列化破坏

    为HungrySingleton类实现Serializable接口进行序列化

    public class HungrySingleton implements Serializable {
        private static HungrySingleton hungrySingleton;
        static {
            hungrySingleton = new HungrySingleton();
        }
        private HungrySingleton() {
    
        }
        public static HungrySingleton getInstance() {
            return hungrySingleton;
        }
    }
    

    测试代码

    import java.io.*;
    
    /**
     * Create by lastwhisper on 2019/1/25
     */
    public class Test {
        public static void main(String[] args) throws Exception {
            HungrySingleton instance = HungrySingleton.getInstance();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\singleton_file"));
            oos.writeObject(instance);
    
            File file = new File("E:\\singleton_file");
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
            HungrySingleton newInstance = (HungrySingleton)ois.readObject();
    
            System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance);
        }
    }
    

    测试结果,发现单例生成的对象与序列化后反序列化回来的对象不一样了。

    序列化攻击测试

    我们为HungrySingleton类添加一个readResolve()方法

    public class HungrySingleton implements Serializable {
        private static HungrySingleton hungrySingleton;
        static {
            hungrySingleton = new HungrySingleton();
        }
        private HungrySingleton() {
    
        }
        public static HungrySingleton getInstance() {
            return hungrySingleton;
        }
    
        private Object readResolve(){
            return hungrySingleton;
        }
    
    }
    

    测试代码不变,再次测试,发现单例生成的对象与序列化后反序列化回来的对象一样了。

    序列化攻击测试

    4.2 返回不同对象原理分析

    在测试代码Test的反序列化方法readObject()里

    readObject()

    readObject()调用readObject0(false);

    readObject0(false)

    在readObject0()里。会进入一个switch,调用checkResolve(readOrdinaryObject(unshared))

    checkResolve(readOrdinaryObject(unshared))

    在readOrdinaryObject()方法里面调用
    obj = desc.isInstantiable() ? desc.newInstance() : null;

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

    desc.isInstantiable(),只要实现serializable/externalizable接口就返回true。
    返回true就会执行desc.newInstance(),obj就会被newInstance()初始化,所以序列化后返回的对象与单例获得对象地址不同

    desc.isInstantiable()

    4.3 返回相同对象原理分析

    既然实现了serializable/externalizable接口,反序列化时就会重新创建对象,造成单例模式创建出不同的对象,为什么加上readResolve()方法就可以单例了呢?

    private Object readResolve(){
        return hungrySingleton;
    }
    

    接着readOrdinaryObject()方法

    readOrdinaryObject()

    desc.hasReadResolveMethod()方法,对于实现了serializable or externalizable接口,同时有readResolve方法的,返回true。进入if判断,执行Object rep = desc.invokeReadResolve(obj);

    desc.hasReadResolveMethod()

    Object rep = desc.invokeReadResolve(obj);中,由于我们有readResolve()方法,会直接执行readResolveMethod.invoke(obj, (Object[]) null);,然后invoke执行我们单例模式本身的readResolve()方法,直接返回hungrySingleton。

    readResolveMethod.invoke(obj, (Object[]) null) readResolveMethod readResolve()

    所以添加readResolve方法,返回了相同对象。

    4.4 总结

    使用序列化时,进行反序列化会使用反射重新创建对象,解决方案就是添加readResolve方法,但是添加readResolve方法,也只是给反射创建的对象覆盖成单例创建的对象,在单例模式使用序列化时一定要注意。

    5. 反射攻击解决方案及原理分析

    反射攻击就是,通过反射创建与单例对象不同的对象,破坏单例模式。
    虽然在单例模式构造器是私有的,但是我们可以通过反射进行修改权限,进行访问。

    5.1 反射攻击

    (1)饿汉式

    使用反射破坏饿汉式单例模式HungrySingleton,编写测试代码

    public class Test {
        public static void main(String[] args) throws Exception {
            /*反射测试*/
            Class objectClass  = HungrySingleton.class;
            
            Constructor constructor = objectClass.getDeclaredConstructor();
            constructor.setAccessible(true);
            HungrySingleton instance = HungrySingleton.getInstance();
            HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
           
             System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance);
        }
    }
    

    测试结果,可以通过反射破坏单例模式饿汉式的对象创建

    反射破坏单例模式饿汉式测试结果

    (2)懒汉式之静态内部类

    静态内部类

    public class Test {
       public static void main(String[] args) throws Exception {
           /*反射测试*/
           Class objectClass  = StaticInnerClassSingleton.class;
    
           Constructor constructor = objectClass.getDeclaredConstructor();
           constructor.setAccessible(true);
           StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
           StaticInnerClassSingleton newInstance = (StaticInnerClassSingleton) constructor.newInstance();
    
           System.out.println(instance);
           System.out.println(newInstance);
           System.out.println(instance == newInstance);
       }
    }
    

    测试结果,单例模式懒汉式的静态内部类实现,也可以通过反射破坏。

    单例模式的静态内部类反射攻击

    (3)双重检测加锁与懒汉式同步锁

    代码与(1)(2)攻击方式类似,不在赘述。

    5.2 解决方案

    反射是通过修改私有构造器的访问权限,破坏单例模式的。我们可以在私有构造器进行一些判断,防止反射修改访问权限,调用私有构造器初始化对象。

    ps:此方式只能防止 类加载时创建单例对象的方式

    **(1) 饿汉式 **

    在私有构造器中判断是否存在已经存在单例对象,如果存在就抛异常。

    public class HungrySingleton implements Serializable {
           private static HungrySingleton hungrySingleton;
        static {
            hungrySingleton = new HungrySingleton();
        }
        private HungrySingleton() {
            if (hungrySingleton != null) {
                throw new RuntimeException("单例构造器禁止反射调用");
            }
        }
        public static HungrySingleton getInstance() {
            return hungrySingleton;
        }
    
    }
    

    测试

    import java.lang.reflect.Constructor;
    
    /**
     * Create by lastwhisper on 2019/1/25
     */
    public class Test {
        public static void main(String[] args) throws Exception {
            /*反射测试*/
            Class objectClass = HungrySingleton.class;
            
            Constructor constructor = objectClass.getDeclaredConstructor();
            constructor.setAccessible(true);
            
            HungrySingleton instance = HungrySingleton.getInstance();
            HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
            System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance);
        }
    }
    

    测试结果,成功抵挡反射攻击。

    饿汉式抵挡反射攻击

    (2) 懒汉式之静态内部类

    同样的思路,在私有构造器中判断是否存在已经存在单例对象,如果存在就抛异常。

    public class StaticInnerClassSingleton {
        private StaticInnerClassSingleton() {
            if (InnerClass.staticInnerClassSingleton != null) {
                throw new RuntimeException("单例构造器禁止反射调用");
            }
        }
    
        private static class InnerClass {
            private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
        }
    
        public static StaticInnerClassSingleton getInstance() {
            return InnerClass.staticInnerClassSingleton;
        }
    }
    

    测试代码

    import java.lang.reflect.Constructor;
    
    /**
     * Create by lastwhisper on 2019/1/25
     */
    public class Test {
        public static void main(String[] args) throws Exception {
            /*反射测试*/
            Class objectClass  = StaticInnerClassSingleton.class;
    
            Constructor constructor = objectClass.getDeclaredConstructor();
            constructor.setAccessible(true);
    
            StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
            StaticInnerClassSingleton newInstance = (StaticInnerClassSingleton) constructor.newInstance();
    
            System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance);
        }
    }
    

    测试结果,成功抵挡反射攻击。

    懒汉式之静态内部类抵挡反射攻击

    (3) 懒汉式之同步锁与双重检测加锁

    很不幸,这两种方式无法抵挡反射攻击,因为这两种方式在类加载时并不创建对象。在私有构造器进行判断的方法只能防止类加载时创建单例对象的方式。

    这里我们以懒汉式之同步锁为例(ps:双重检测锁也相同)。
    在私有构造器中添加判断

    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
        private LazySingleton() {
            if (lazySingleton != null) {
                throw new RuntimeException("单例构造器禁止反射调用");
            }
    
        }
    
        public synchronized static LazySingleton getInstance() {
            if (lazySingleton == null) {
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    }
    

    测试代码

    import java.lang.reflect.Constructor;
    
    /**
     * Create by lastwhisper on 2019/1/25
     */
    public class Test {
        public static void main(String[] args) throws Exception {
            /*反射测试*/
            Class objectClass = LazySingleton.class;
    
            Constructor constructor = objectClass.getDeclaredConstructor();
            constructor.setAccessible(true);
    
            LazySingleton instance = LazySingleton.getInstance();
            LazySingleton newInstance = (LazySingleton) constructor.newInstance();
    
            System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance);
        }
    }
    

    测试结果

    懒汉式之同步锁抵挡反射攻击测试结果

    看似抵挡了反射攻击。
    我们来交换一下测试代码这两行代码执行顺序。

    LazySingleton newInstance = (LazySingleton) constructor.newInstance();
    LazySingleton instance = LazySingleton.getInstance();
    

    变换为:

    LazySingleton instance = LazySingleton.getInstance();
    LazySingleton newInstance = (LazySingleton) constructor.newInstance();
    

    测试结果,无法阻止反射攻击。

    懒汉式之同步锁抵挡反射攻击测试结果

    双重检测加锁效果也类似,无法抵挡反射攻击。

    5.3 原理分析与扩展

    (1)原理分析

    我们接着“5.2.3 懒汉式之同步锁与双重检测加锁”,来分析下测试代码。

    Class objectClass = LazySingleton.class;
    Constructor constructor = objectClass.getDeclaredConstructor();
    constructor.setAccessible(true);
    LazySingleton newInstance = (LazySingleton) constructor.newInstance();
    

    这几行代码,会通过反射创建LazySingleton对象,但是静态私有变量lazySingleton还是为null。

    private static LazySingleton lazySingleton = null;
    

    我们使用反射创建对象与getInstance()创建对象,打印一下私有静态变量lazySingleton(暂时将权限设为public,测试一下私有静态变量lazySingleton)

    public static LazySingleton lazySingleton = null;
    
    私有静态变量lazySingleton

    所以将反射创建对象代码constructor.newInstance()放在LazySingleton.getInstance()之前,constructor.newInstance()创建LazySingleton对象的静态私有变量lazySingleton为null,LazySingleton.getInstance()创建对象调用私有构造器时if判断失效。

    如果是多线程情况下,thread1执行constructor.newInstance()在thread2执行LazySingleton.getInstance()之前,私有构造器判断失效。所以如果不是类加载时初始化单例类(比如懒汉式之同步锁与双重检测加锁),是无法阻止反射攻击。

    (2)扩展1

    不知道有没有人比较较真,增加私有静态成员变量,增强私有构造器的判断。我们增加一个flag标志(ps:使用更复杂逻辑道理也是相同)。

    public class LazySingleton {
        public static LazySingleton lazySingleton = null;
        private static boolean flag = true;
    
        private LazySingleton() {
            if (flag) {
                flag = false;
            } else {
                throw new RuntimeException("单例构造器禁止反射调用");
            }
        }
    
        public synchronized static LazySingleton getInstance() {
            if (lazySingleton == null) {
                lazySingleton = new LazySingleton();
            }
            return lazySingleton;
        }
    
    }
    

    这种方式同样会被反射攻击,因为反射可以修改权限设置值。

    测试代码

    public static void main(String[] args) throws Exception {
        Class objectClass = LazySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazySingleton o1 = LazySingleton.getInstance();
        //修改flag=true
        Field flag = o1.getClass().getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(o1,true);
    
        LazySingleton o2 = (LazySingleton) constructor.newInstance();
    
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1 == o2);
    }
    

    测试结果,反射攻击成功,无法阻止反射攻击。

    测试结果

    (3)扩展2

    那为什么不可以通过反射设置静态私有变量lazySingleton的值为自己创建的值呢?哪样所有的私有构造器方法判断都会失效,即使类加载时初始化单例类也无法阻止反射攻击?像这样。

    Field lazySingleton = o1.getClass().getDeclaredField("lazySingleton");
    lazySingleton.setAccessible(true);
    lazySingleton.set(o1,new LazySingleton());
    

    哈哈哈哈哈哈啊哈哈哈,报错了吧,忘了我们的构造器是私有了的么。

    'LazySingleton()' has private access in 
    'com.desgin.pattern.creational.singleton.LazySingleton'
    

    6. 单例模式的最佳实践

    序列化与反序列化:懒汉式之同步锁、双重检测加锁、静态内部类与饿汉式都必须增加一个readResolve()方法,不然反序列化回来的不是同一个对象。并且就算是增加了readResolve()方法反序列化时也会newInstance一个对象,只不过被readResolve()返回的单例对象覆盖。

    反射攻击:懒汉式之同步锁、双重检测加锁由于不是在类加载时初始化单例对象,无法阻止反射攻击。懒汉式之静态内部类与饿汉式需要在私有构造器增加判断,可以防止反射攻击。

    上面四种单例模式方式需要根据不同业务场景使用相对应的单例模式实现。

    6.1 最佳实践

    下面介绍一种单例模式的最佳实践(ps:也是《EffectiveJava》推荐的单例实现方式)

    Enum实现单例模式

    public enum EnumInstance {
        INSTANCE;
        private Object data;
    
        public Object getData() {
            return data;
        }
    
        public void setData(Object data) {
            this.data = data;
        }
        public static EnumInstance getInstance(){
            return INSTANCE;
        }
    }
    

    6.2 序列化攻击

    (1)测试

    使用序列化与反序列化测试一下会不会出问题。我们先测试这个枚举持有的INSTANCE

    import java.io.*;
    
    /**
     * Create by lastwhisper on 2019/1/26
     */
    public class Test1 {
        public static void main(String[] args) throws Exception {
            EnumInstance instance = EnumInstance.getInstance();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\singleton_file"));
            oos.writeObject(instance);
    
            File file = new File("E:\\singleton_file");
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
            EnumInstance newInstance = (EnumInstance)ois.readObject();
            System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance);
        }
    }
    

    测试结果,序列化与反序列化并不会破坏单例模式

    序列化攻击Enum实现单例模式

    再测试枚举持有的对象data,看看这个data是不是同一个

    import java.io.*;
    
    /**
     * Create by lastwhisper on 2019/1/26
     */
    public class Test1 {
        public static void main(String[] args) throws Exception {
            EnumInstance instance = EnumInstance.getInstance();
            instance.setData(new Object());
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\singleton_file"));
            oos.writeObject(instance);
    
            File file = new File("E:\\singleton_file");
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
            EnumInstance newInstance = (EnumInstance)ois.readObject();
            System.out.println(instance.getData());
            System.out.println(newInstance.getData());
            System.out.println(instance.getData() == newInstance.getData());
        }
    }
    

    测试结果,是同一个data。

    序列化攻击Enum实现单例模式

    (2)原理分析

    readObject()

    在测试类的readObject()方法中,会调用Object obj = readObject0(false);

    Object obj = readObject0(false)

    readObject0()方法,进入switch,case TC_ENUM

    case TC_ENUM

    readEnum()方法里,进入一系列校验。在1715行String name = readString(false);,通过readString()方法获取枚举对象的名称name。在1716行Enum en = null;声明一个Enum类型。在1717行Class cl = desc.forClass();获取枚举对象的类型。在1720行en = Enum.valueOf(cl, name);根据类型和name,对枚举常量进行初始化。没有创建新的对象,维持了单例属性。

    readEnum()

    6.3 反射攻击

    (1)测试以及原理分析

    import java.lang.reflect.Constructor;
    
    /**
     * Create by lastwhisper on 2019/1/25
     */
    public class Test {
        public static void main(String[] args) throws Exception {
            /*反射测试*/
            Class objectClass = EnumInstance.class;
            Constructor constructor = objectClass.getDeclaredConstructor();
            constructor.setAccessible(true);
            EnumInstance newInstance = (EnumInstance) constructor.newInstance();
        }
    }
    
    

    测试结果,抛出异常NoSuchMethodException,获取构造器时没有获得无参构造器。

    抛出异常NoSuchMethodException

    为什么会这样的呢?我们进入java.lang.Enum的源码中看一下。
    在Enum类中只有一个有参构造器

    Enum的有参构造器 Enum的有参构造器

    修改测试代码,构造一个有参构造器

    Class objectClass = EnumInstance.class;
    
    Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class);
    constructor.setAccessible(true);
    EnumInstance newInstance = (EnumInstance) constructor.newInstance("gaojun",123456);
    

    测试结果,异常信息:Cannot reflectively create enum objects

    测试结果

    我们点进520行错误代码里面,发现如果是Enum类型Coustructor的newInstance方法就会抛出异常,Cannot reflectively create enum objects。所以无法通过反射创建Enum类型。

    无法通过反射创建Enum类型

    6.4 Enum实现单例模式的优势

    我们使用jad对EnumInstance进行反编译,查看Enum做单例的优势。

    jad EnumInstance.class
    
    jad EnumInstance.class

    打开生成的jad文件。

    // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
    // Jad home page: http://www.kpdus.com/jad.html
    // Decompiler options: packimports(3) 
    // Source File Name:   EnumInstance.java
    package com.desgin.pattern.creational.singleton;
    
    public final class EnumInstance extends Enum
    {
    
        public static EnumInstance[] values()
        {
            return (EnumInstance[])$VALUES.clone();
        }
    
        public static EnumInstance valueOf(String name)
        {
            return (EnumInstance)Enum.valueOf(com/desgin/pattern/creational/singleton/EnumInstance, name);
        }
    
        private EnumInstance(String s, int i)
        {
            super(s, i);
        }
    
        public Object getData()
        {
            return data;
        }
    
        public void setData(Object data)
        {
            this.data = data;
        }
    
        public static EnumInstance getInstance()
        {
            return INSTANCE;
        }
        public static final EnumInstance INSTANCE;
        private Object data;
        private static final EnumInstance $VALUES[];
        
        static 
        {
            INSTANCE = new EnumInstance("INSTANCE", 0);
            $VALUES = (new EnumInstance[] {
                INSTANCE
            });
        }
    }
    
    

    首先EnumInstance类是final类型的无法被继承,有一个私有构造器。

    private EnumInstance(String s, int i)
        {
            super(s, i);
        }
    

    以及静态的final的单例对象,在类被加载时就会被静态代码块(ps:static{})初始化,并且不可被修改,保证了线程安全。加上I/O类、反射类对Enum类型的支持,Enum非吧常适合做单例模式。

    public static final EnumInstance INSTANCE;
        private Object data;
        private static final EnumInstance $VALUES[];
    
        static 
        {
            INSTANCE = new EnumInstance("INSTANCE", 0);
            $VALUES = (new EnumInstance[] {
                INSTANCE
            });
        }
    
    

    Enum单例实现优势总结

    1. 写法简单
    2. 线程安全
    3. 懒加载
    4. 避免序列化攻击
    5. 避免反射攻击

    7. 容器单例

    将单例对象都保存在一个容器中

    package com.desgin.pattern.creational.singleton;
    import org.apache.commons.lang3.StringUtils;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * Create by lastwhisper on 2019/1/27
     */
    public class ContainerSingleton {
        private static Map<String, Object> singletonMap = new HashMap<String, Object>();
    
        private ContainerSingleton() {
        }
        public static void putInstance(String key, String instance){
            if(StringUtils.isNoneBlank(key) && instance != null){
                if(!singletonMap.containsKey(key)){
                    singletonMap.put(key,instance);
                }
            }
        }
        public static Object getInstance(String key){
            return singletonMap.get(key);
        }
    }
    
    

    由于HashMap线程不安全,导致这种容器单例模式也是线程不安全的,这种场景适用于,项目初始化时将需要的单例对象放入Map中。如果改有HashTable,虽然线程安全,但在频繁get的过程会有同步锁,效率低。如果改用CurrentHashMap,此时是静态的CurrentHashMap,并且是直接操作的CurrentHashMap,CurrentHashMap并不是绝对的线程安全。

    8. 线程单例

    这种方式只能保证在一个线程内拿到单例对象

    public class ThreadLocalInstance {
        private static final ThreadLocal<ThreadLocalInstance> treadLocalInstance =
                new ThreadLocal<ThreadLocalInstance>() {
                    @Override
                    protected ThreadLocalInstance initialValue() {
                        return new ThreadLocalInstance();
                    }
                };
    
        private ThreadLocalInstance() {
        }
    
        public static ThreadLocalInstance getInstance() {
            return treadLocalInstance.get();
        }
    }
    

    9. 优缺点

    优点:

    • 在内存里只有一个实例,减少了内存开销
    • 可以避免对资源的多重占用
    • 设置全局访问点,严格控制访问

    缺点:

    • 没有接口,扩展困难

    10. 扩展-JDK1.7源码中的单例模式

    10.1 Runtime——单例模式的饿汉式

    通过查看java.lang.Runtime静态成员变量currentRuntime、getRunTime()方法、私有构造器,可知是一个单例模式的饿汉式。

    Runtime

    10.2 Desktop——容器单例

    10.2 Desktop——容器单例
    查看 java.awt.Desktop类的getDesktop()方法,是一个同步方法,会从一个AppContext中取值,如果context中没有就new一个,并put进context中。

    getDesktop()

    查看put方法,会将值put到this.table中。

    put方法

    查看this.table是一个HashMap。是一个容器单例,只不过put方法里面加了同步锁,保证put时是线程安全的。

    table

    10.3 ErrorContext——线程单例

    org.apache.ibatis.executor.ErrorContext类中使用ThreadLocal保证线程安全,调用instance()方法创建单例的ErrorContext对象,每个线程自己的错误,线程自己保存。

    public class ErrorContext {
    
      private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
      private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
    
      private ErrorContext stored;
      private String resource;
      private String activity;
      private String object;
      private String message;
      private String sql;
      private Throwable cause;
    
      private ErrorContext() {
      }
    
      public static ErrorContext instance() {
        ErrorContext context = LOCAL.get();
        if (context == null) {
          context = new ErrorContext();
          LOCAL.set(context);
        }
        return context;
      }
    }
    

    10.4 AbstractFactoryBean——懒汉式

    org.springframework.beans.factory.config.AbstractFactoryBeangetObject() 方法中,查看调用getEarlySingletonInstance()

    getObject()

    使用了懒汉式,初始化单例对象

    getEarlySingletonInstance()

    11. 单例模式总结

    11.1 单例模式实现方法

    单例模式实现方法

    11.2 安全性

    ** 序列化与反序列化:**

    • 懒汉式之同步锁、双重检测加锁、静态内部类与饿汉式都必须增加一个readResolve()方法,不然反序列化回来的不是同一个对象。并且就算是增加了readResolve()方法反序列化时也会newInstance一个对象,只不过被readResolve()返回的单例对象覆盖。
    • 枚举实现不会被序列化与反序列化影响

    反射攻击:

    • 懒汉式之同步锁、双重检测加锁由于不是在类加载时初始化单例对象,无法阻止反射攻击。
    • 懒汉式之静态内部类与饿汉式需要在私有构造器增加判断,可以防止反射攻击。
    • 枚举类无法反射创建对象,所有不会被反射影响。

    11.3 扩展-CAS实现单例

    上面的所有实现的单例方法本质上都使用的是锁,不使用锁的话,有办法实现线程安全的单例吗?

    有,那就是使用CAS。

    CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。实现单例的方式如下:

    package com.desgin.pattern.creational.singleton;
    
    import java.util.concurrent.atomic.AtomicReference;
    
    /**
     * @author lastwhisper
     *
     */
    public class CASSingleton {
        private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<CASSingleton>();
    
        private CASSingleton() {
        }
    
    
        public static CASSingleton getInstance() {
            for (;;) {
                CASSingleton singleton = INSTANCE.get();
                if (null != singleton) {
                    return singleton;
                }
    
                singleton = new CASSingleton();
                if (INSTANCE.compareAndSet(null, singleton)) {
                    return singleton;
                }
            }
        }
    }
    

    这种方式实现的单例有啥优缺点吗?

    用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。

    CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

    另外,如果N个线程同时执行到singleton = new Singleton();的时候,会有大量对象创建,很可能导致内存溢出。所以,不建议使用这种实现方式。

    参考

    geely Java设计模式精讲 Debug方式+内存分析 的单例模式

    面试官:不使用synchronized和lock,如何实现一个线程安全的单例?

    相关文章

      网友评论

          本文标题:创建型模式——单例模式(四)

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