單例模式是應用最爲普遍的模式之一,也多是不少入門或初級工程師惟一會使用的設計模式之吧,在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只須要擁有一個實例類。有利於咱們的調用,避免一個相同的類重複建立實例,好比一個網絡請求,圖片請求/下載,數據庫操做等,若是頻繁建立同一個相同對象的話,很消耗資源,所以,沒有理由讓它們構造多個實例。全局都須要使用這個功能的時候,避免重複建立,就能夠用單例,這就是單例使用場景。java
確保某個類只有一個實例,並且自行實例化並向整個系統提供這個實例。android
應用中重複使用某個類時,爲了不屢次建立產生的資源消耗,那麼這個時候就能夠考慮使用單例設計模式。git
實現單例模式主要有以下幾個關鍵點:github
單例模式是設計模式中比較簡單的,只有一個單例類,沒有其餘層次結構與抽象。該模式須要確保該類只能生成一個對象,一般是該類須要消耗較多的資源或者沒有多個實例的狀況。例以下面的代碼:數據庫
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 都進行同步,形成沒必要要的開銷。這種模式通常不建議使用。
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裏面的執行分爲三步:
因爲在 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 種場景下會對類進行初始化。
咱們再回頭看下 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 源碼中涉及了大量的單例模式,這裏就拿較爲熟悉的 context.getSystemService(String name); 容器單例模式,以 Context.LAYOUT_INFLATER_SERVICE 舉例。
從 setContentView 入口,全方位分析 LayoutInflater
單例模式在應用中時屬於使用頻率最高的一種設計模式了,可是因爲客戶端一般沒有高併發的狀況,所以,選擇哪一種實現方式並不會有太大的影響。固然,考慮效率和併發的場景仍是推薦你們使用 DCL 或 靜態內部類單例模式。
注意:若是單例對象必須持有參數的話,那麼最好建議使用弱引用來接收參數,若是是 Context 級別的類型,建議使用 context.getApplication() 不然容易形成內存泄漏;
感謝你的閱讀,謝謝!