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


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

 

  归纳层面:

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

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

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

    


 

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

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

  -- 是否实现延迟加载

  -- 是否线程安全

  -- 并发访问性能

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

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

 

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

 

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

 

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2020 CODEPRJ.COM