設計模式 (一) 經過理論 + 代碼示例 + Android 源碼中單例模式來學習單例

介紹

單例模式是應用最爲普遍的模式之一,也多是不少入門或初級工程師惟一會使用的設計模式之吧,在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只須要擁有一個實例類。有利於咱們的調用,避免一個相同的類重複建立實例,好比一個網絡請求,圖片請求/下載,數據庫操做等,若是頻繁建立同一個相同對象的話,很消耗資源,所以,沒有理由讓它們構造多個實例。全局都須要使用這個功能的時候,避免重複建立,就能夠用單例,這就是單例使用場景。java

定義

確保某個類只有一個實例,並且自行實例化並向整個系統提供這個實例。android

使用場景

應用中重複使用某個類時,爲了不屢次建立產生的資源消耗,那麼這個時候就能夠考慮使用單例設計模式。git

單例 UML 類圖

czjKW.png

實現單例模式主要有以下幾個關鍵點:github

  1. 構造函數不對外開放,通常爲 private;
  2. 經過一個靜態方法或者枚舉返回單例對象;
  3. 確保單例類的對象有且只有一個,尤爲是在多線程環境下;
  4. 確保單例類對象在反序列化時不會被從新構建對象。

單例示例

餓漢式

單例模式是設計模式中比較簡單的,只有一個單例類,沒有其餘層次結構與抽象。該模式須要確保該類只能生成一個對象,一般是該類須要消耗較多的資源或者沒有多個實例的狀況。例以下面的代碼:數據庫

public class DaoManager {

    /** * 餓漢式單例 */
    private static DaoManager instance = new DaoManager();

    private DaoManager(){}

    public static DaoManager getInstance(){
        return instance;
    }
}
複製代碼

測試編程

@Test
    public void test(){
        String dao = DaoManager.getInstance().toString();
        String dao1 = DaoManager.getInstance().toString();
        String dao2 = DaoManager.getInstance().toString();
        String dao3 = DaoManager.getInstance().toString();


        System.out.println(dao);
        System.out.println(dao1);
        System.out.println(dao2);
        System.out.println(dao3);
    }
複製代碼

Output設計模式

com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
com.devyk.android_dp_code.singleton.DaoManager@28ba21f3
複製代碼

從上面代碼能夠看到 DaoManager 不能經過 new 的形式構造對象,只能經過 getInstance() 拿到實例,而 DaoManager 對象是靜態的,那麼在聲明的時候已經初始化了,這就保證了對象的惟一性,從輸入結果中發現, DaoManager 四次輸出的地址都是同樣的。這個實現的核心在與將 DaoManager 類的構造方法私有化,使得外部程序不能經過構造來 new 對象,只能經過 getInstance() 來返回一個對象。安全

懶漢模式

懶漢模式是聲明瞭一個靜態對象,而且在第一調用的時候進行初始化,而上面的餓漢紙則是在聲明的時候已經初始化了。懶漢式的實現以下:網絡

public class DaoManager2 {
    
    private static DaoManager2 instance;
    
    private DaoManager2(){}

    /** * 保證線程安全的懶漢式 * @return */
    public static synchronized DaoManager2 getInstance(){
        if (null == instance) {
            instance = new DaoManager2();
        }
        return instance;
    }
}
複製代碼

細心的讀者可能已經發現了,getInstance() 方法中添加了 synchronized 關鍵字, getInstance 是一個同步方法,保證了在多線程狀況下單例對象惟一性。細想下,你們可能會發現一個問題,即便 instance 已經被初始化,每次調用都會進行同步檢查,這樣會消耗沒必要要的資源,這也是懶漢單例模式存在的最大問題。多線程

最後總結一下,懶漢單例模式的優勢是單例只有再使用的時候進行初始化,在必定程度上節約了資源;缺點是第一次加載時須要進行初始化,反應稍慢,最大的問題就是每次調用的時候 getInstance 都進行同步,形成沒必要要的開銷。這種模式通常不建議使用。

Double Check Lock 實現單例

DCL 方式實現單例模式的有點是既可以在須要時初始化單例,又能保證線程安全,且單例對象初始化後調用 instance 不進行同步鎖,代碼以下:

public class DaoManager3 {

    private static DaoManager3 sinstance;

    private DaoManager3() {
    }

    /** * 保證線程安全的懶漢式 * * @return */
    public static DaoManager3 getInstance() {
        if (null == sinstance) {
            synchronized (DaoManager3.class) {
                if (null == instance)
                    sinstance = new DaoManager3();
            }
        }
        return sinstance;
    }
}

複製代碼

本段代碼的亮點就在於 getInstance 方法上,能夠看到 getInstance 方法對 instance 進行了兩次判空;第一層判斷主要是爲了不沒必要要的同步,第二層的判斷則是爲了在 null 的狀況下建立實例。是否是看起來有點迷糊,下面在來解釋下:

sinstance = new DaoManager3();
複製代碼

這個步驟,其實在jvm裏面的執行分爲三步:

  1. 在堆內存開闢內存空間;
  2. 在堆內存中實例化 DaoManager3 裏面的各個參數;
  3. 把對象指向堆內存空間;

因爲在 JDK 1.5 之前 Java 編譯器容許處理器亂序執行,以及 JMM 沒法保證 Cache, 寄存器(Java 內存模型)保證按照 1,2,3 的順序執行。因此可能在 2 還沒執行時就先執行了 3,若是此時再被切換到線程 B 上,因爲執行了 3,sinstance 已經非空了,會被直接拿出來用,這樣的話,就會出現異常。並且不易復現不易跟蹤是一個隱藏的 BUG。

不過在 JDK 1.5 以後,官方也發現了這個問題,故而具體化了 volatile ,即在 JDK 1.6 之後,只要定義爲 private volatile static DaoManager3 sinstance ; 就可解決 DCL 失效問題。volatile 確保 sinstance 每次均在主內存中讀取,這樣雖然會犧牲一點效率,但也無傷大雅。

DCL 優勢:資源利用率高,第一次執行 getInstance 時單例對象纔會被實例化,效率高。

DCL 缺點:第一次加載時,反應稍慢,也因爲 Java 內存模型的緣由偶爾會失敗。在高併發環境下也有必定的缺陷,雖然發生機率很小。

DCL 模式是使用最多的模式,它可以在須要時才被實例化,而且可以在絕大多數場景下保證單例對象的惟一性,除非你的代碼在併發場景比較複雜或者低於 JDK 6 版本下使用,不然,這種方式通常可以知足需求。

靜態內部類單例模式

DCL 雖然在必定程度上解決了資源消耗、多餘的同步、線程安全等問題,可是,它仍是在某些狀況下出現失效的問題,這個問題被稱爲雙重檢查鎖定失效,在《Java 併發編程實踐》一書的最後談到了這個問題,並指出這種 「優化」 是醜陋的,不同意使用。而建議使用以下的代碼替代。

public class DaoManager4 {
    
    private DaoManager4(){}

    public static DaoManager4 getInstance(){
        return DaoManager4Holder.sInstance;
    }

    /** * 靜態內部類 * */
    private static class DaoManager4Holder{
        private static final DaoManager4 sInstance = new DaoManager4();
    }
}
複製代碼

那麼,靜態內部類又是如何實現線程安全的呢?首先,咱們先了解下類的加載時機。

類加載時機:JAVA 虛擬機在有且僅有的 5 種場景下會對類進行初始化。

  1. 遇到 new、getstatic、setstatic 或者 invokestatic 這4個字節碼指令時,對應的 java 代碼場景爲:new 一個關鍵字或者一個實例化對象時、讀取或設置一個靜態字段時 ( final 修飾、已在編譯期把結果放入常量池的除外)、調用一個類的靜態方法時。
  2. 使用 java.lang.reflect 包的方法對類進行反射調用的時候,若是類沒進行初始化,須要先調用其初始化方法進行初始化。
  3. 當初始化一個類時,若是其父類還未進行初始化,會先觸發其父類的初始化。
  4. 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的類),虛擬機會先初始化這個類。
  5. 當使用 JDK 1.7 等動態語言支持時,若是一個 java.lang.invoke.MethodHandle 實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。 這 5 種狀況被稱爲是類的主動引用,注意,這裏《虛擬機規範》中使用的限定詞是"有且僅有",那麼,除此以外的全部引用類都不會對類進行初始化,稱爲被動引用。靜態內部類就屬於被動引用的行列。

咱們再回頭看下 getInstance() 方法,調用的是 DaoManager4Holder.sInstance ,取的是DaoManager4Holder 裏的 sInstance 對象,跟上面那個 DCL 方法不一樣的是 ,getInstance()方法並無屢次去 new 對象,故無論多少個線程去調用 getInstance() 方法,取的都是同一個sInstance 對象,而不用去從新建立。當 getInstance() 方法被調用時,DaoManager4Holder 纔在 DaoManager4 的運行時常量池裏,把符號引用替換爲直接引用,這時靜態對象sInstance 也真正被建立,而後再被 getInstance() 方法返回出去,這點同餓漢模式。那麼sInstance 在建立過程當中又是如何保證線程安全的呢?在《深刻理解JAVA虛擬機》中,有這麼一句話:

虛擬機會保證一個類的 () 方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的 () 方法,其餘線程都須要阻塞等待,直到活動線程執行 () 方法完畢。若是在一個類的 () 方法中有耗時很長的操做,就可能形成多個進程阻塞 (須要注意的是,其餘線程雖然會被阻塞,但若是執行 () 方法後,其餘線程喚醒以後不會再次進入 () 方法。同一個加載器下,一個類型只會初始化一次。),在實際應用中,這種阻塞每每是很隱蔽的。

故而,能夠看出 sInstance 在建立過程當中是線程安全的,因此說靜態內部類形式的單例可保證線程安全,也能保證單例的惟一性,同時也延遲了單例的實例化。

那麼,是否是能夠說靜態內部類單例就是最完美的單例模式了呢?其實否則,靜態內部類也有着一個致命的缺點,就是傳參的問題,因爲是靜態內部類的形式去建立單例的,故外部沒法傳遞參數進去,例如 Context 這種參數,因此,咱們建立單例時,能夠在靜態內部類與 DCL 模式裏本身斟酌。

枚舉單例

前面講解了幾個單例模式的實現方式,這幾個實現方式不是稍顯麻煩就是會在某種狀況下出現問題,那麼還有沒有更簡單的實現方式勒? 咱們先來看看下面的實現方式。

public enum  DaoManager5 {
    
    INSTANCE;
    
    public void doSomething(){
        Log.i("DAO->","枚舉單例");
    }
}
複製代碼

沒錯,就是枚舉單例!

寫法簡單簡單是枚舉單例最大的優勢,枚舉在 Java 中與普通的類時同樣的,不只可以擁有字段,還可以擁有本身的方法。最重要的是默認枚舉實例的建立是線程安全的,而且在任何狀況下它都是一個單例。

優勢:枚舉自己是線程安全的,且能防止經過反射和反序列化建立實例。

缺點:對 JDK 版本有限制要求,非懶加載。

使用容器實現單例模式

學習了上面 5 大單例模式,最後在來介紹一種容器單例模式,請看下面代碼實現:

public class DaoManager6 {

    /** * 定義一個容器 */
    private static Map<String,Object> singletonMap = new HashMap<>();
    
    private DaoManager6(){}
    
    public static void initDao(String key,Object instance){
        if (!singletonMap.containsKey(key)){
            singletonMap.put(key,instance);
        }
    }
    
    public static Object getDao(String key){
        return singletonMap.get(key);
    }
}
複製代碼

在程序的初始,能夠將單例類型注入到統一管理類中,在使用的時候根據 key 獲取對應單例對象,而且在使用時能夠經過統一的接口進行獲取操做,下降了用戶的使用成本,也對用戶隱藏了具體實現,下降了耦合度。

Android 源碼中單例模式

Android 源碼中涉及了大量的單例模式,這裏就拿較爲熟悉的 context.getSystemService(String name); 容器單例模式,以 Context.LAYOUT_INFLATER_SERVICE 舉例。

從 setContentView 入口,全方位分析 LayoutInflater

總結

單例模式在應用中時屬於使用頻率最高的一種設計模式了,可是因爲客戶端一般沒有高併發的狀況,所以,選擇哪一種實現方式並不會有太大的影響。固然,考慮效率和併發的場景仍是推薦你們使用 DCL 或 靜態內部類單例模式。

注意:若是單例對象必須持有參數的話,那麼最好建議使用弱引用來接收參數,若是是 Context 級別的類型,建議使用 context.getApplication() 不然容易形成內存泄漏;

文章中出現的代碼

感謝你的閱讀,謝謝!

相關文章
相關標籤/搜索