• 关于几种常见的单例模式的学习总结


      单例模式——顾名思义即在既定的业务场景下某一实体类只需存在一个对象,就能充分的处理所有的业务需求。而且在某种现场环境下,创建这样的对象对系统性能的开销非常大。正因为这种特性,单利模式通常具有节省系统开销的效果。我将从以下几个方面对一些常见的单利模式进行总结归纳,在下才疏学浅,不曾卖弄,旨在知识重温与记录。有所疏忽,请各位不吝指正,自当感激不尽。

      归纳层面:

        常见的单利模式以及实现方式。

        产品级单例模式的穿透方式以及防范方法。

        常见的单利模式的并发性能测试。

        


    一,常见的单利模式以及实现方式

      在实现层面上,目前的几种主要的单例模式往往有以下几项性能指标作为选型参考:

      -- 是否实现延迟加载

      -- 是否线程安全

      -- 并发访问性能

      -- 是否可以防止反射与反序列化穿透

      经过一段时间的工作和学习,将自己所遇到的几种单例模式作如下比较总结,当然,也作为自己学习复习的一种方式。

      <1>,饿汉式单例模式。

    /**
     * 未实现延迟加载
     * 线程安全
     * @author xinz
     *
     */
    public class Singleton1 {
    
        private Singleton1(){}
        
        private static Singleton1 instance = new Singleton1();
        
        public static Singleton1 getInstance(){
            return instance;
        }
    }

      <2>,懒汉式单例模式

    /**
     * 实现延迟加载
     * 线程安全但牺牲高并发性能
     * @author xinz
     */
    public class Singleton2 {
    
        private Singleton2(){        
        }
        
        private static Singleton2 instance;
        
        public static synchronized Singleton2 getInstance(){
            if(instance == null){
                instance = new Singleton2();
            }
            return instance;
        }
    }

      <3>,双重检测锁式单例模式

    /**
     * 双重检测锁式单例模式
     * 实现了延迟加载 
     * 线程安全
     * @author xinz
     *
     */
    public class Singleton3 {
    
        private static Singleton3 instance = null;
    
        private Singleton3() {}
    
        public static Singleton3 getInstance() {
            if (instance == null) {
                Singleton3 sc;
                synchronized (Singleton3.class) {
                    sc = instance;
                    if (sc == null) {
                        synchronized (Singleton3.class) {
                            if (sc == null) {
                                sc = new Singleton3();
                            }
                        }
                        instance = sc;
                    }
                }
            }
            return instance;
        }
    
    }

      <4>,静态内部类式单例模式

    /**
     * 静态内部类单利模式
     * 线程安全
     * 实现延迟加载
     * @author xinz
     *
     */
    public class Singleton4 {
    
        private Singleton4 (){}
        
        /**
         * 外部类初始化的时候不会初始化该内部类
         * 只有当调用getInstance方法时候才会初始化
         */
        public static class inner{
            public static final Singleton4 instance = new Singleton4();
        }
        
        public static Singleton4 getInstance(){
            return inner.instance;
        }
    }

      <5>,枚举式单例模式

    /**
     * 未延迟加载
     * 线程安全
     * 原生防止反射与反序列话击穿
     * @author xinz
     */
    public enum Singleton5 {
    
        INSTANCE;
        
        public static Object doSomething(){
            
            //添加其他功能逻辑。。。。。。
            
            return null;
        }
    }

    对于以上5种单例模式作如下简单测试:

    /**
     * 测试单利是否返回相同对象
     * @author xinz
     *
     */
    public class TestSingleton {
    
        public static void main(String[] args) {
            /**
             * 饿汉式
             */
            Singleton1 singleton1_1 = Singleton1.getInstance(); 
            Singleton1 singleton1_2 = Singleton1.getInstance(); 
            System.out.println(singleton1_1 == singleton1_1);//true
            
            /**
             * 懒汉式
             */
            Singleton2 singleton2_1 = Singleton2.getInstance(); 
            Singleton2 singleton2_2 = Singleton2.getInstance(); 
            System.out.println(singleton2_1 == singleton2_1);//true
            
            /**
             * 双重检测锁式
             */
            Singleton3 singleton3_1 = Singleton3.getInstance(); 
            Singleton3 singleton3_2 = Singleton3.getInstance(); 
            System.out.println(singleton3_1 == singleton3_1);//true
            
            /**
             * 静态内部类式
             */
            Singleton4 singleton4_1 = Singleton4.getInstance(); 
            Singleton4 singleton4_2 = Singleton4.getInstance(); 
            System.out.println(singleton4_1 == singleton4_1);//true
    
            /**
             * 枚举式
             */
            Singleton5 singleton5_1 = Singleton5.INSTANCE; 
            Singleton5 singleton5_2 = Singleton5.INSTANCE; 
            
            /*
             * 枚举型的任何成员类型都是类实例的类型
             */
            System.out.println(singleton5_1.getClass());//class com.xinz.source.Singleton5
            
            System.out.println(singleton5_1 == singleton5_1);//true
        }
    }

      综上,5种实现单例模式的方法,都能基本实现现有系统目标对象的唯一性。区别在于是否能够延迟加载进一步节约系统性能。其中“双重检测锁式”由于JVM底层在执行同步块的嵌套时有时会发生漏洞,所以在JDK修复该漏洞之前,该方式不建议使用。

    二,产品级单例模式的穿透方式以及防范方法


      

      关于以上的五种单利模式的实现方式,对一般的Web应用开发,我们无需考虑谁会来试图破解我们的单利限制。但如果开发是面向产品级,那么我们将不得不考虑单例破解问题,常见的单例模式多见于反射穿透与序列化破解。

      <1>,防止反射穿透。

      对于反射,我们知道只要有构造方法,不做处理的情况下,即使私有化构造器,也没办阻止反射调用得到对象。从而使既有系统存在多个对象。如下,我们使用饿汉式单例模式为例,进行反射穿透。代码如下:

    /*
     * 反射破解饿汉式单例模式
     */
    public class TestReflectSingleton {
    
        public static void main(String[] args) throws Exception {
            
            Class<Singleton1> clazz = (Class<Singleton1>) Class.forName("com.xinz.source.Singleton1");
            
            Constructor<Singleton1> constructor = clazz.getDeclaredConstructor(null);
            
            //强制设置构造器可访问
            constructor.setAccessible(true);
            
            Singleton1 s1 = constructor.newInstance();
            Singleton1 s2 = constructor.newInstance();
            
            System.out.println(s1==s2);//false
        }
    }

      那么很显然,像饿汉式,懒汉式,双重检测锁式,静态内部类事,这几种只要有构造器的单例模式就会存在被反射穿透的风险。而第五种枚举式单例模式,原生不存在构造器,所以避免了反射穿透的风险。

      对于前边四种存在反射穿透的单例模式,我们的解决思路就是,万一有人通过反射进入到构造方法,那么我们可以考虑抛异常,代码如下:

    /**
     * 实现延迟加载
     * 线程安全但牺牲高并发性能
     * @author xinz
     */
    public class Singleton2 {
    
        /*
         * 如果有反射进入构造器,判断后抛异常,这样的话一旦初始化 instance 对象
         * 反射调用便会被阻止,初始化之前还是可以被反射的
         */
        private Singleton2(){
            if(instance != null){
                throw new RuntimeException();
            }
        }
        
        private static Singleton2 instance;
        
        public static synchronized Singleton2 getInstance(){
            if(instance == null){
                instance = new Singleton2();
            }
            return instance;
        }
    }

      测试代码:

    import java.lang.reflect.Constructor;
    
    public class TestSingleton {
    
        public static void main(String[] args) throws Throwable {
            Singleton2 s1 = Singleton2.getInstance();
            Singleton2 s2 = Singleton2.getInstance();
            
            System.out.println(s1);
            System.out.println(s1);
            
            Class<Singleton2> clazz = (Class<Singleton2>) Class.forName("com.xinz.source.Singleton2");
            Constructor<Singleton2> c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            Singleton2 s3 = c.newInstance();
            Singleton2 s4 = c.newInstance();
            
            System.out.println(s3);
            System.out.println(s4);
            
        }
        
    }

      执行结果:

    com.xinz.source.Singleton2@2542880d
    com.xinz.source.Singleton2@2542880d
    Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
        at com.xinz.source.TestSingleton.main(TestSingleton.java:17)
    Caused by: java.lang.RuntimeException
        at com.xinz.source.Singleton2.<init>(Singleton2.java:15)
        ... 5 more

      即一旦初始化完成后,反射就会报错。但无法阻止反射发生在初始化之前,代码如下:

    import java.lang.reflect.Constructor;
    
    public class TestSingleton {
    
        public static void main(String[] args) throws Throwable {
            
            Class<Singleton2> clazz = (Class<Singleton2>) Class.forName("com.xinz.source.Singleton2");
            Constructor<Singleton2> c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            Singleton2 s3 = c.newInstance();
            Singleton2 s4 = c.newInstance();
            
            System.out.println(s3);
            System.out.println(s4);
            
            Singleton2 s1 = Singleton2.getInstance();
            Singleton2 s2 = Singleton2.getInstance();
            
            System.out.println(s1);
            System.out.println(s1);
        }
        
    }

    测试结果如下:

    com.xinz.source.Singleton2@32f22097
    com.xinz.source.Singleton2@3639b3a2
    com.xinz.source.Singleton2@6406c7e
    com.xinz.source.Singleton2@6406c7e

      很显然反射得到的两个对象不是同一对象。目前尚未找到解决策略,还望高手指点。

      <2>,反序列化破解

      反序列化即先将系统里边唯一的单实例对象序列化到硬盘,然后在反序列化,得到的对象默认和原始对象属性一致,但已经不是同一对象了。如下:

    /**
     * 反序列化创建新对象
     * @author xizn
     */
    public class TestSingleton {
    
        public static void main(String[] args) throws Throwable {
            
            Singleton2 s1 = Singleton2.getInstance();
            System.out.println(s1);
            
            //通过反序列化的方式构造多个对象 
            FileOutputStream fos = new FileOutputStream("d:/a.txt");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s1);
            oos.close();
            fos.close();
            
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
            Singleton2 s3 =  (Singleton2) ois.readObject();
            System.out.println(s3);
        }
    }

      测试结果如下:(当然,目标类要实现序列化接口)

    com.xinz.source.Singleton2@3639b3a2
    com.xinz.source.Singleton2@46e5590e

      如何防止这种破解单利模式,我们采取重写反序列化方法 -- readResolve() 最终防止单利被破解的代码如下(这里仅以懒汉式为例,其它类似):

    import java.io.ObjectStreamException;
    import java.io.Serializable;
    
    /**
     * 实现延迟加载
     * 线程安全但牺牲高并发性能
     * @author xinz
     */
    public class Singleton2 implements Serializable {
    
        /*
         * 如果有反射进入构造器,判断后抛异常,这样的话一旦初始化 instance 对象
         * 反射调用便会被阻止,初始化之前还是可以被反射的
         */
        private Singleton2(){
            if(instance != null){
                throw new RuntimeException();
            }
        }
        
        private static Singleton2 instance;
        
        public static synchronized Singleton2 getInstance(){
            if(instance == null){
                instance = new Singleton2();
            }
            return instance;
        }
        
        //反序列化时,如果定义了readResolve()则直接返回此方法指定的对象。而不需要单独再创建新对象!
        private Object readResolve() throws ObjectStreamException {
            return instance;
        }
    }

      还是上边的测试代码,测试结果:

    com.xinz.source.Singleton2@6f92c766
    com.xinz.source.Singleton2@6f92c766

    三,常见的单利模式的并发性能测试


      

      测试我们启用20个线程,每个线程循环获取单例对象100万次,测试代码:

    /**
     * 并发性能测试
     * @author xizn
     */
    public class TestSingleton {
    
        public static void main(String[] args) throws Throwable {
            
            long start = System.currentTimeMillis();
            int threadNum = 20;
            final CountDownLatch  countDownLatch = new CountDownLatch(threadNum);
            
            for(int i=0;i<threadNum;i++){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        
                        for(int i=0;i<1000000;i++){
    //                        Object o1 = Singleton1.getInstance();
    //                        Object o2 = Singleton2.getInstance();
    //                        Object o3 = Singleton3.getInstance();
    //                        Object o4 = Singleton4.getInstance();
                            Object o5 = Singleton5.INSTANCE;
                        }
                        
                        countDownLatch.countDown();
                    }
                }).start();
            }
            
            countDownLatch.await();    //main线程阻塞,直到计数器变为0,才会继续往下执行!
            
            long end = System.currentTimeMillis();
            System.out.println("总耗时:"+(end-start));
        }
    }

      执行结果根据电脑性能每个人可能会有不同的结果,但大概还是可以反映出性能优劣:

    并发性能测试
    饿汉式 总耗时:10毫秒 不支持延迟加载,一般不能防范反射与反序列化
    懒汉式 总耗时:498毫秒 支持延迟加载,一般不能防范反射与反序列化,并发性能差
    双重检测锁式 总耗时:11毫秒 JVM底层支持不太好,其它性能同饿汉式
    静态内部类式 总耗时:12毫秒 一般不能防范反射与反序列化,其它性能良好
    枚举式 总耗时:12毫秒 未实现延迟加载,原生防范反射与反序列化,其它性能良好

      综上测试结果,个人认为:

        对于要求延迟加载的系统,静态内部类式优于懒汉式。

        对于产品级别,要求安全级别高的系统,枚举式优于饿汉式。

        双重检测锁式单例模式在JDK修复同步块嵌套漏洞之前不推荐

      写了大半天,总算对自己的学习内容总结告一段落,在此,特别感谢高淇、白鹤翔两位老师。

     

  • 相关阅读:
    中国跨境电商物流难题的三大解决方案
    美团外卖实时数仓建设实践
    美团商品知识图谱的构建及应用
    c++动态创建二维数组
    小鱼儿fish C#获取数组大小
    C#中定义数组--字符串及数组操作
    warning LNK4070的解决办法
    C++ 中 ZeroMemory、memset 危险需慎用
    C++中new和delete来创建和释放动态数组
    VS调试时查看动态数组的全部元素
  • 原文地址:https://www.cnblogs.com/UYGHYTYH/p/5912548.html
Copyright © 2020-2023  润新知