單例模式

單例模式(Singleton)相信大家或多或少都用過,代碼量不多饲做,看起來也很簡單线婚,但是里面的學(xué)問卻不簡單。面試中經(jīng)常會問到單例模式相關(guān)的問題盆均,但是如果你只停留在最簡單的使用上塞弊,那么想讓面試官滿意就很難了。
單例模式用來保證應(yīng)用中有且僅有一個實例泪姨,也是Android中用的較多的一種設(shè)計模式游沿。單例模式有很多使用場景,例如肮砾,當(dāng)一個對象需要頻繁創(chuàng)建诀黍、銷毀時,為了減少內(nèi)存開銷仗处,可以使用單例模式蔗草。
單例模式最關(guān)鍵的兩點:
**1咒彤、私有構(gòu)造方法
2、公有的對外調(diào)用接口
**

單例模式分為兩種咒精,餓漢式和懶漢式镶柱。常見簡單寫法如下所示:

一、餓漢式

 public class Singleton{
      private static final Singleton instance = new Singleton();  //聲明為static和final模叙,第一次加載類到內(nèi)存中時就會初始化
      private Singleton(){}
      public static Singleton newInstance(){
             return instance; 
      }
}

二歇拆、懶漢式(線程不安全)

 public class Singleton{
      private static Singleton instance = null;
      private Singleton(){}
      public static Singleton newInstance(){
              if(instance == null){
                    instance = new Singleton();   //方法被調(diào)用的時候才生成實例
                                   } 
              return instance;
         }
}

三、餓漢式與懶漢式的區(qū)別

對比這兩種方式的代碼我們可以看出范咨,餓漢式在類第一次加載進內(nèi)存就實例化了故觅,而懶漢式使用了懶加載模式,當(dāng)方法被調(diào)用時類對象的實例時才生成渠啊。根據(jù)名稱也很好理解输吏,餓漢式很餓,剛一加載進內(nèi)存替蛉,不等你說贯溅,就迫不及待生成“食物”(類的實例)充饑;懶漢式很懶躲查,需要的時候你催我(調(diào)用獲取實例函數(shù))它浅,我才給你生成。

四镣煮、餓漢式與懶漢式的適用場景

餓漢式是最簡單的實現(xiàn)方式姐霍,適合那些在初始化時就要用到單例的情況,如果單例對象初始化非车浯剑快镊折,而且占用內(nèi)存非常小的時候這種方式是比較合適的,可以直接在應(yīng)用啟動時加載并初始化介衔。餓漢式的創(chuàng)建方式在一些場景中無法使用:譬如 Singleton 實例的創(chuàng)建是依賴參數(shù)或者配置文件的恨胚,在 getInstance() 之前必須調(diào)用某個方法設(shè)置參數(shù)給它,那么這種單例寫法就無法使用了夜牡。
  懶漢式將單例的初始化操作延遲到需要的時候才進行与纽,若某個單例用的次數(shù)不是很多,但是這個單例提供的功能又非常復(fù)雜塘装,而且加載和初始化要消耗大量的資源急迂,這個時候使用懶漢式就比使用餓漢式合適的多。

五蹦肴、線程安全的懶漢式

不知道大家發(fā)現(xiàn)沒有僚碎,在上面的代碼中關(guān)于懶漢式的寫法存在一個致命的缺點,那就是在多線程的情況下無法正常使用阴幌,當(dāng)多個線程同時調(diào)用getInstance()方法時勺阐,就會創(chuàng)建多個Singleton實例卷中,因此這種寫法是線程不安全的。而餓漢式只會在Singleton類加載進內(nèi)存時實例化一次渊抽,不會出現(xiàn)多次實例化的問題蟆豫,也就不存在線程安全問題。

1懒闷、同步鎖

為了解決懶漢式的線程安全問題十减,最簡單的方法是使用同步鎖synchronized

public class Singleton {
     private static Singleton instance = null;
     private Singleton(){ }
     public static Singleton getInstance() {
              synchronized (Singleton.class) {//防止多線程同時進入造成instance被多次實例化
                  if (instance == null) {
                       instance = new Singleton();
                                } 
                    }
              return instance;
        }
}

2愤估、雙重檢驗鎖(Double-Check)

這樣做雖然解決了線程安全問題帮辟,但是并不高效,因為在任何時候只能有一個線程調(diào)用 getInstance() 方法玩焰。我們的同步并不是要防止多個線程同時調(diào)用getInstance()由驹,而是防止多個線程同時實例化instatnce。因此昔园,可以在實例化instatnce的地方進行同步蔓榄,如下所示:

public class Singleton {
     private static Singleton instance = null; 
     private Singleton(){ }
     public static Singleton getInstance() {
          if (instance == null) { //-------------------Single Checked
           // 若實例創(chuàng)建了,則不需要同步了蒿赢,直接返回instance即可
                synchronized (Singleton.class) { //未創(chuàng)建實例润樱,加鎖
                     if (instance == null) { //----------------------Double Checked
                     //若被同步的線程中有一個線程創(chuàng)建了實例渣触,那么別的線程就不用創(chuàng)建了
                              instance = new Singleton(); 
                          }
                   } 
           }
           return instance;
      }
}

其中第二次Check是因為可能會有多個線程一起進入同步塊外的 if羡棵,如果在同步塊內(nèi)不進行二次檢驗的話就會生成多個實例了。

3嗅钻、懶漢式的最佳寫法

上面的Double-check看似完美了皂冰,但是卻存在問題,因為 instance = new Singleton();這句代碼并不是原子操作养篓。在JVM中這句代碼大概做了下面 3 件事情:
(1)給 instance 分配內(nèi)存
(2)調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量秃流,形成實例
(3)將instance對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 才為非 null)
但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化。也就是說上面的第(2)步和第(3)步的順序是不能保證的柳弄,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2舶胀。如果是后者,則在 3 執(zhí)行完畢碧注、2 未執(zhí)行之前嚣伐,被其他線程搶占了,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化)萍丐,所以其他線程會直接返回 instance轩端,然后使用這個未初始化的instance,就會順理成章地報錯逝变。
解決辦法也很簡單基茵,只需要將 instance 變量聲明成 volatile 就可以了奋构。我們一般使用volatile關(guān)鍵字有兩個功能:
(1)可見性:volatile關(guān)鍵字修飾的變量不會在多個線程中存在副本,每次都是直接從內(nèi)存中讀取拱层。
(2)禁止指令重排序優(yōu)化弥臼。在 volatile 變量的賦值操作后面會有一個內(nèi)存屏障(生成的匯編代碼上),讀操作不會被重排序到內(nèi)存屏障之前根灯。比如上面的例子醋火,取操作必須在執(zhí)行完 1-2-3 之后或者 1-3-2 之后,不存在執(zhí)行到 1-3 然后取到值的情況箱吕。

public class Singleton {
     private volatile static Singleton instance = null; //聲明成 volatile
     private Singleton(){ }
     public static Singleton getInstance() {
          if (instance == null) { //-------------------Single Checked
           // 若實例創(chuàng)建了芥驳,則不需要同步了,直接返回instance即可
                synchronized (Singleton.class) { //未創(chuàng)建實例茬高,加鎖
                     if (instance == null) { //----------------------Double Checked
                     //若被同步的線程中有一個線程創(chuàng)建了實例兆旬,那么別的線程就不用創(chuàng)建了
                              instance = new Singleton(); 
                          }
                   } 
           }
           return instance;
      }
}

六、靜態(tài)內(nèi)部類

public class Singleton {
     private static class SingletonHolder {//只有加載內(nèi)部類的時候才初始化
          private static final Singleton INSTANCE = new Singleton();
     } 
     private Singleton (){}
     public static final Singleton getInstance() { //只有在getInstance()被調(diào)用的時候才被真正創(chuàng)建
          return SingletonHolder.INSTANCE; 
     }
}

這種寫法仍然使用JVM本身機制保證了線程安全問題怎栽。
  由于 SingletonHolder 是私有的丽猬,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的熏瞄;同時讀取實例的時候不會進行同步脚祟,沒有性能缺陷;也不依賴 JDK 版本强饮。這種寫法即解決了餓漢式不能延遲加載的缺陷由桌,又解決了懶漢式線程安全的問題,也是《Effective Java》上所推薦的寫法邮丰。

七行您、枚舉(Enum)

public enum Singleton{ 
      INSTANCE; //定義一個枚舉的元素,它就是Singleton的一個實例 
}

我們可以通過Singleton.INSTANCE來訪問實例剪廉,這比調(diào)用getInstance()方法簡單多了娃循。默認(rèn)枚舉實例的創(chuàng)建是線程安全的(創(chuàng)建枚舉類的單例在JVM層面也是能保證線程安全的),所以不需要擔(dān)心線程安全的問題斗蒋,而且還能防止反序列化導(dǎo)致重新創(chuàng)建新的對象捌斧。所以理論上枚舉類來實現(xiàn)單例模式是最簡單的方式。但由于大多數(shù)人不太熟悉泉沾,用這種方法寫的人不多捞蚂。

八、總結(jié)

一般來說爆哑,單例模式有五種寫法:餓漢洞难、懶漢、雙重檢驗鎖、靜態(tài)內(nèi)部類队贱、枚舉色冀。上述所說都是線程安全的實現(xiàn),文章開頭給出的第一種懶漢式方法不算正確的寫法柱嫌。
  在我們?nèi)粘嵺`中锋恬,一般情況下直接使用餓漢式就好了,如果明確要求要懶加載编丘,則推薦使用靜態(tài)內(nèi)部類与学,如果涉及到反序列化創(chuàng)建對象時,可以使用枚舉的方式來實現(xiàn)單例嘉抓。但是在很多的面試中索守,線程安全的懶漢式是最常考察的抑片,我們需要好好掌握Double-Check的寫法卵佛,注意關(guān)鍵字volatile的使用。
參考文章:
1敞斋、http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
2截汪、http://stormzhang.com/designpattern/2016/03/27/android-design-pattern-singleton/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市植捎,隨后出現(xiàn)的幾起案子衙解,更是在濱河造成了極大的恐慌,老刑警劉巖焰枢,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蚓峦,死亡現(xiàn)場離奇詭異,居然都是意外死亡医咨,警方通過查閱死者的電腦和手機枫匾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門架诞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拟淮,“玉大人,你說我怎么就攤上這事谴忧『懿矗” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵沾谓,是天一觀的道長委造。 經(jīng)常有香客問我,道長均驶,這世上最難降的妖魔是什么昏兆? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮妇穴,結(jié)果婚禮上爬虱,老公的妹妹穿的比我還像新娘隶债。我一直安慰自己,他們只是感情好跑筝,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布死讹。 她就那樣靜靜地躺著,像睡著了一般曲梗。 火紅的嫁衣襯著肌膚如雪赞警。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天虏两,我揣著相機與錄音愧旦,去河邊找鬼。 笑死定罢,一個胖子當(dāng)著我的面吹牛忘瓦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播引颈,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼耕皮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蝙场?” 一聲冷哼從身側(cè)響起凌停,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎售滤,沒想到半個月后罚拟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡完箩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年赐俗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弊知。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡阻逮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出秩彤,到底是詐尸還是另有隱情叔扼,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布漫雷,位于F島的核電站瓜富,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏降盹。R本人自食惡果不足惜与柑,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧价捧,春花似錦每辟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至椎眯,卻和暖如春挠将,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背编整。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工舔稀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人掌测。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓内贮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親汞斧。 傳聞我的和親對象是個殘疾皇子夜郁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容