關於幾種常見的單例模式的學習總結


  單例模式——顧名思義即在既定的業務場景下某一實體類只需存在一個對象,就能充分的處理所有的業務需求。而且在某種現場環境下,創建這樣的對象對系統性能的開銷非常大。正因為這種特性,單利模式通常具有節省系統開銷的效果。我將從以下幾個方面對一些常見的單利模式進行總結歸納,在下才疏學淺,不曾賣弄,旨在知識重溫與記錄。有所疏忽,請各位不吝指正,自當感激不盡。

 

  歸納層面:

    常見的單利模式以及實現方式。

    產品級單例模式的穿透方式以及防范方法。

    常見的單利模式的並發性能測試。

    


 

一,常見的單利模式以及實現方式

  在實現層面上,目前的幾種主要的單例模式往往有以下幾項性能指標作為選型參考:

  -- 是否實現延遲加載

  -- 是否線程安全

  -- 並發訪問性能

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

  經過一段時間的工作和學習,將自己所遇到的幾種單例模式作如下比較總結,當然,也作為自己學習復習的一種方式。

 

  <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-2025 CODEPRJ.COM