Android 架構師之路5 設計模式之單例模式

Android 架構師之路 目錄

前言

Java中單例(Singleton)模式是一種廣泛使用的設計模式。單例模式的主要作用是保證在Java程序中趴久,某個類只有一個實例存在缀旁。一些管理器和控制器常被設計成單例模式鸳谜。
單例模式有很多好處本刽,它能夠避免實例對象的重復創(chuàng)建,不僅可以減少每次創(chuàng)建對象的時間開銷亭饵,還可以節(jié)約內(nèi)存空間休偶;能夠避免由于操作多個實例導致的邏輯錯誤。如果一個對象有可能貫穿整個應用程序辜羊,而且起到了全局統(tǒng)一管理控制的作用踏兜,那么單例模式也許是一個值得考慮的選擇。

1八秃、單例模式UML類圖

單例模式UML類圖

2碱妆、單例模式的八種寫法

2.1餓漢模式

顧名思義,餓漢法就是在第一次引用該類的時候就創(chuàng)建對象實例昔驱,而不管實際是否需要創(chuàng)建疹尾。代碼如下:

public class Singleton {   
    private static Singleton = new Singleton();
    private Singleton() {}
    public static getSignleton(){
        return singleton;
    }
}

這樣做的好處是編寫簡單,但是無法做到延遲創(chuàng)建對象。但是我們很多時候都希望對象可以盡可能地延遲加載纳本,從而減小負載窍蓝,所以就需要下面的懶漢法。

2.2 餓漢模式變種
public class Singleton {  
    private Singleton instance = null;  
     static {  
    instance = new Singleton();  
    }  
    private Singleton (){}
    public static Singleton getInstance() {  
    return this.instance;  
    }  
 }  

表面上看起來差別挺大繁成,其實上面那種差不多吓笙,都是在類初始化即實例化instance。.

2.3不加鎖懶漢模式(線程不安全)

懶漢模式中單例是在需要的時候才去創(chuàng)建的朴艰,如果單例已經(jīng)創(chuàng)建,再次調(diào)用獲取接口將不會重新創(chuàng)建新的對象混移,而是直接返回之前創(chuàng)建的對象祠墅。如果某個單例使用的次數(shù)少,并且創(chuàng)建單例消耗的資源較多歌径,那么就需要實現(xiàn)單例的按需創(chuàng)建毁嗦,這個時候使用懶漢模式就是一個不錯的選擇。但是這里的懶漢模式并沒有考慮線程安全問題回铛,在多個線程可能會并發(fā)調(diào)用它的getInstance()方法狗准,就有很大可能導致重復創(chuàng)建對象蔼囊。

public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton() {
        if(singleton == null) singleton = new Singleton();
        return singleton;
    }
}

2.4加鎖懶漢模式(線程安全哟旗,但是耗時)

這種寫法考慮了線程安全魄藕,將對singleton的null判斷以及new的部分使用synchronized進行加鎖衔瓮。同時挑社,對singleton對象使用volatile關鍵字進行限制五慈,保證其對所有線程的可見性泛释,并且禁止對其進行指令重排序優(yōu)化胳嘲。如此即可從語義上保證這種單例模式寫法是線程安全的您没。但是每次通過getInstance方法得到singleton實例的時候都有一個試圖去獲取同步鎖的過程鸟召。而眾所周知,加鎖是很耗時的氨鹏,對高并發(fā)操作很不友好欧募。

public class Singleton {
    private static volatile Singleton singleton = null;
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }    
}

2.5雙重校驗鎖( 兼顧線程安全和效率的寫法)

雖然上面這種寫法是可以正確運行的,但是其效率低下仆抵,還是無法實際應用跟继。因為每次調(diào)用getSingleton()方法,都必須在synchronized這里進行排隊镣丑,而真正遇到需要new的情況是非常少的还栓。所以,就誕生了第三種寫法:

public class Singleton {
    private static volatile Singleton singleton = null;
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

這種寫法被稱為“雙重檢查鎖”传轰,顧名思義剩盒,就是在getSingleton()方法中,進行兩次null檢查×闪模看似多此一舉纪挎,但實際上卻極大提升了并發(fā)度,進而提升了性能跟匆。為什么可以提高并發(fā)度呢异袄?就像上文說的,在單例中new的情況非常少玛臂,絕大多數(shù)都是可以并行的讀操作烤蜕。因此在加鎖前多進行一次null檢查就可以減少絕大多數(shù)的加鎖操作,執(zhí)行效率提高的目的也就達到了迹冤。

該種寫法存在Java低版本中的問題
內(nèi)存模型

那么讽营,這種寫法是不是絕對安全呢?前面說了泡徙,從語義角度來看橱鹏,并沒有什么問題。但是其實還是有坑堪藐。說這個坑之前我們要先來看看volatile這個關鍵字莉兰。其實這個關鍵字有兩層語義。第一層語義相信大家都比較熟悉礁竞,就是可見性糖荒。可見性指的是在一個線程中對該變量的修改會馬上由工作內(nèi)存(Work Memory)寫回主內(nèi)存(Main Memory)模捂,所以會馬上反應在其它線程的讀取操作中寂嘉。順便一提,工作內(nèi)存和主內(nèi)存可以近似理解為實際電腦中的高速緩存和主存枫绅,工作內(nèi)存是線程獨享的泉孩,主存是線程共享的。volatile的第二層語義是禁止指令重排序優(yōu)化并淋。大家知道我們寫的代碼(尤其是多線程代碼)寓搬,由于編譯器優(yōu)化,在實際執(zhí)行的時候可能與我們編寫的順序不同县耽。編譯器只保證程序執(zhí)行結(jié)果與源代碼相同句喷,卻不保證實際指令的順序與源代碼相同。這在單線程看起來沒什么問題兔毙,然而一旦引入多線程唾琼,這種亂序就可能導致嚴重問題。volatile關鍵字就可以從語義上解決這個問題澎剥。

例如锡溯,考慮下面的事件序列:

  1. 線程A發(fā)現(xiàn)變量沒有被初始化, 然后它獲取鎖并開始變量的初始化。
  2. 由于某些編程語言的語義,編譯器生成的代碼允許在線程A執(zhí)行完變量的初始化之前祭饭,更新變量并將其指向部分初始化的對象芜茵。
  3. 線程B發(fā)現(xiàn)共享變量已經(jīng)被初始化,并返回變量倡蝙。由于線程B確信變量已被初始化九串,它沒有獲取鎖。如果在A完成初始化之前共享變量對B可見(這是由于A沒有完成初始化或者因為一些初始化的值還沒有穿過B使用的內(nèi)存(緩存一致性))寺鸥,程序很可能會崩潰猪钮。
Symantec JIT 編譯 singletons[i].reference = new Singleton(); 這段代碼時,如果不加volatile關鍵詞胆建,會生成如下字節(jié)碼:

0206106A   mov         eax,0F97E78h
0206106F   call        01F6B210                  ; allocate space for
                                                 ; Singleton, return result in eax
02061074   mov         dword ptr [ebp],eax       ; EBP is &singletons[i].reference 
                                                ; store the unconstructed object here.
02061077   mov         ecx,dword ptr [eax]       ; dereference the handle to
                                                 ; get the raw pointer
02061079   mov         dword ptr [ecx],100h      ; Next 4 lines are
0206107F   mov         dword ptr [ecx+4],200h    ; Singleton's inlined constructor
02061086   mov         dword ptr [ecx+8],400h
0206108D   mov         dword ptr [ecx+0Ch],0F84030h
可以看到烤低,在執(zhí)行Singleton的構造函數(shù)之前,Singleton的新實例就被賦值給了singletons[i].reference眼坏,這在Java內(nèi)存模型中是完全合法的拂玻。

注意酸些,前面反復提到“從語義上講是沒有問題的”宰译,但是很不幸,禁止指令重排優(yōu)化這條語義直到jdk1.5以后才能正確工作魄懂。此前的JDK中即使將變量聲明為volatile也無法完全避免重排序所導致的問題沿侈。所以,在jdk1.5版本前市栗,雙重檢查鎖形式的單例模式是無法保證線程安全的缀拭。

2.6 靜態(tài)內(nèi)部類法(推薦)

那么,有沒有一種延時加載填帽,并且能保證線程安全的簡單寫法呢蛛淋?我們可以把Singleton實例放到一個靜態(tài)內(nèi)部類中,這樣就避免了靜態(tài)實例在Singleton類加載的時候就創(chuàng)建對象篡腌,并且由于靜態(tài)內(nèi)部類只會被加載一次褐荷,所以這種寫法也是線程安全的:

public class Singleton {
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

但是,上面提到的所有實現(xiàn)方式都有兩個共同的缺點:

  • 都需要額外的工作(Serializable嘹悼、transient叛甫、readResolve())來實現(xiàn)序列化,否則每次反序列化一個序列化的對象實例時都會創(chuàng)建一個新的實例杨伙。
  • 可能會有人使用反射強行調(diào)用我們的私有構造器(如果要避免這種情況其监,可以修改構造器,讓它在創(chuàng)建第二個實例的時候拋異常)限匣。
2.7 枚舉寫法

當然抖苦,還有一種更加優(yōu)雅的方法來實現(xiàn)單例模式,那就是枚舉寫法:

    public  class Resource{
    }

    public enum SomeThing {
        INSTANCE;
        private Resource instance;
        SomeThing() {
            instance = new Resource();
        }
        public Resource getInstance() {
            return instance;
        }
    }

調(diào)用

    Resource resource = SomeThing.INSTANCE.getInstance();

使用枚舉除了線程安全和防止反射強行調(diào)用構造器之外,還提供了自動序列化機制睛约,防止反序列化的時候創(chuàng)建新的對象鼎俘。因此,Effective Java推薦盡可能地使用枚舉來實現(xiàn)單例辩涝。

2.8 容器實現(xiàn)單例模式
import java.util.HashMap;
import java.util.Map;

public class Singleton {
    private static Map<String, Object> objMap = new HashMap<String, Object>();

    private Singleton() {
    }

    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);
        }
    }

    public static Object getService(String key) {
        return objMap.get(key);
    }
}

這種實現(xiàn)方式使得我們可以管理多種類型的單例贸伐,并且在使用時可以通過統(tǒng)一接口進行獲取操作,降低用戶使用成本怔揩,也對用戶隱藏了具體實現(xiàn)捉邢,降低耦合度。

3 商膊、單例模式在Android源碼中應用

第三方 ImageLoader(通過源碼分析伏伐,得到單例模式中雙重檢測方案)
LayoutInflater 單例模式通過容器進行管理
LayoutInflater 源碼分析 WindowManager、ActivityManager晕拆、PowerManager都是容器管理

總結(jié)

代碼沒有一勞永逸的寫法藐翎,只有在特定條件下最合適的寫法。在不同的平臺实幕、不同的開發(fā)環(huán)境(尤其是jdk版本)下吝镣,自然有不同的最優(yōu)解(或者說較優(yōu)解)。
比如枚舉昆庇,雖然Effective Java中推薦使用末贾,但是在Android平臺上卻是不被推薦的。在這篇Android Training中明確指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再比如雙重檢查鎖法整吆,不能在jdk1.5之前使用拱撵,而在Android平臺上使用就比較放心了(一般Android都是jdk1.6以上了,不僅修正了volatile的語義問題表蝙,還加入了不少鎖優(yōu)化拴测,使得多線程同步的開銷降低不少)。

最后府蛇,不管采取何種方案集索,請時刻牢記單例的三大要點:

  • 線程安全
  • 延遲加載
  • 序列化與反序列化安全
特別感謝:

吃桔子的攻城獅

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市欲诺,隨后出現(xiàn)的幾起案子抄谐,更是在濱河造成了極大的恐慌,老刑警劉巖扰法,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛹含,死亡現(xiàn)場離奇詭異,居然都是意外死亡塞颁,警方通過查閱死者的電腦和手機浦箱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門吸耿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人酷窥,你說我怎么就攤上這事咽安。” “怎么了蓬推?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵妆棒,是天一觀的道長。 經(jīng)常有香客問我沸伏,道長糕珊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任毅糟,我火速辦了婚禮红选,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘姆另。我一直安慰自己喇肋,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布迹辐。 她就那樣靜靜地躺著蝶防,像睡著了一般。 火紅的嫁衣襯著肌膚如雪右核。 梳的紋絲不亂的頭發(fā)上慧脱,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天渺绒,我揣著相機與錄音贺喝,去河邊找鬼。 笑死宗兼,一個胖子當著我的面吹牛躏鱼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播殷绍,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼染苛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了主到?” 一聲冷哼從身側(cè)響起茶行,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎登钥,沒想到半個月后畔师,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡牧牢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年看锉,在試婚紗的時候發(fā)現(xiàn)自己被綠了姿锭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡伯铣,死狀恐怖呻此,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情腔寡,我是刑警寧澤焚鲜,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站放前,受9級特大地震影響恃泪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜犀斋,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一贝乎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧叽粹,春花似錦览效、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至辆脸,卻和暖如春但校,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背啡氢。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工状囱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人倘是。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓亭枷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親搀崭。 傳聞我的和親對象是個殘疾皇子叨粘,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345