zoukankan      html  css  js  c++  java
  • 关于几种常见的单例模式的学习总结

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

      归纳层面:

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

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

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

        


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

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

      -- 是否实现延迟加载

      -- 是否线程安全

      -- 并发访问性能

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

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

      <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修复同步块嵌套漏洞之前不推荐

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

     

  • 相关阅读:
    HDU 1010 Tempter of the Bone
    HDU 4421 Bit Magic(奇葩式解法)
    HDU 2614 Beat 深搜DFS
    HDU 1495 非常可乐 BFS 搜索
    Road to Cinema
    Sea Battle
    Interview with Oleg
    Spotlights
    Substring
    Dominating Patterns
  • 原文地址:https://www.cnblogs.com/UYGHYTYH/p/5912548.html
Copyright © 2011-2022 走看看