單例模式(Singleton Pattern)

一、簡述

單例模式是 Java 中最簡單的設(shè)計(jì)模式之一。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類提供了一種訪問其唯一對(duì)象的方式,可以直接訪問,不需要實(shí)例化該類的對(duì)象。即兩私一公:①私有的構(gòu)造函數(shù)②私有靜態(tài)實(shí)例屬性③公共的獲取實(shí)例的靜態(tài)方法。

1??注意

  1. 單例類只能有一個(gè)實(shí)例。
  2. 單例類必須自己創(chuàng)建自己的唯一實(shí)例。
  3. 單例類必須給所有其他對(duì)象提供這一實(shí)例。

2??資源加載和性能

餓漢式在類創(chuàng)建的同時(shí)就實(shí)例化一個(gè)靜態(tài)對(duì)象出來,無論會(huì)不會(huì)用到,都會(huì)占據(jù)一定的內(nèi)存。相應(yīng)的,在第一次調(diào)用時(shí)速度更快,因?yàn)槠滟Y源已經(jīng)初始化完成。

懶漢式,會(huì)延遲加載,在初次使用該單例時(shí)才會(huì)實(shí)例化對(duì)象出來。首次調(diào)用時(shí)要做初始化,如果要做的工作比較多,性能會(huì)有所延遲,之后就和餓漢式一樣了。

主要解決:一個(gè)全局使用的類頻繁地創(chuàng)建與銷毀。
何時(shí)使用:當(dāng)想控制實(shí)例數(shù)目,節(jié)省系統(tǒng)資源的時(shí)候。
如何解決:判斷系統(tǒng)是否已經(jīng)有這個(gè)單例,有則返回,沒有則創(chuàng)建。
關(guān)鍵代碼:構(gòu)造函數(shù)是私有的。

3??應(yīng)用實(shí)例

  1. 一個(gè)男人只能有一個(gè)妻子。
  2. Windows 是多進(jìn)程多線程的,難免會(huì)出現(xiàn)多個(gè)進(jìn)程或線程同時(shí)操作一個(gè)文件的現(xiàn)象,所以所有文件的處理必須通過唯一的實(shí)例來進(jìn)行。
  3. 一些設(shè)備管理器常常設(shè)計(jì)為單例模式。比如一個(gè)電腦有兩臺(tái)打印機(jī),在輸出的時(shí)候就要處理不能兩臺(tái)打印機(jī)打印同一個(gè)文件。

4??優(yōu)點(diǎn)

  1. 在內(nèi)存里只有一個(gè)實(shí)例,減少了內(nèi)存的開銷,尤其是頻繁的創(chuàng)建和銷毀實(shí)例(比如管理學(xué)院首頁頁面緩存)。
  2. 避免對(duì)資源的多重占用(比如寫文件操作)。

5??缺點(diǎn)

沒有接口,不能繼承,與單一職責(zé)原則沖突,一個(gè)類應(yīng)該只關(guān)心內(nèi)部邏輯,而不關(guān)心外面怎么樣來實(shí)例化。

6??使用場景

  1. 要求生產(chǎn)唯一序列號(hào)。
  2. WEB 中的計(jì)數(shù)器,不用每次刷新都在數(shù)據(jù)庫里加一次,用單例先緩存起來。
  3. 創(chuàng)建的一個(gè)對(duì)象需要消耗的資源過多,比如 I/O 與數(shù)據(jù)庫的連接等。

二、餓漢式---線程安全

餓漢就是類一旦加載,就把單例初始化完成,保證 getInstance 的時(shí)候,單例已經(jīng)存在。

  • 描述:這種方式比較常用,但容易產(chǎn)生垃圾對(duì)象。
  • 優(yōu)點(diǎn):沒有加鎖,執(zhí)行效率會(huì)提高。
  • 缺點(diǎn):類加載時(shí)就初始化,浪費(fèi)內(nèi)存。
  • 它基于 classloder 機(jī)制避免了多線程的同步問題。不過,instance 在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化 instance 顯然沒有達(dá)到 lazy loading 的效果。
public class EagerSigleton() {
    //持有自己的引用
    private static final EagerSigleton m_instatnce = new EagerSigleton();
    //構(gòu)造器私有化,不能在類的外部隨意創(chuàng)建對(duì)象
    private EagerSigleton() {
    }
    //提供一個(gè)全局的訪問點(diǎn)來獲得這個(gè)“唯一”的對(duì)象
    public static EagerSigleton getInstance() {
        System.out.println("加載餓漢式....");
        return m_instatnce;
    }
}

靜態(tài)代碼塊實(shí)現(xiàn):

public class EagerSigleton{
    private static EagerSigletoninstance;
    static {
        m_instatnce = new EagerSigleton();
    }
    private EagerSigleton() {}
    public static EagerSigletongetInstance() {
        return m_instatnce;
    }
}

這種方式和上面的方式其實(shí)類似,只不過將類實(shí)例化的過程放在了靜態(tài)代碼塊中,也是在類裝載的時(shí)候,就執(zhí)行靜態(tài)代碼塊中的代碼,初始化類的實(shí)例。優(yōu)缺點(diǎn)和上面是一樣的。

三、懶漢式---非線程安全

  • Lazy 初始化。
  • 描述:最基本的實(shí)現(xiàn)方式,最大的問題就是不支持多線程。因?yàn)闆]有加 synchronized 鎖,所以嚴(yán)格意義上它并不算單例模式。
  • 這種方式 lazy loading 很明顯,不要求線程安全,在多線程不能正常工作。
public class LazySigleton(){
  private static LazySigleton instatnce=null;
  // 構(gòu)造器私有化,不能在類的外部隨意創(chuàng)建對(duì)象
  private LazySigleton(){}
  // 提供一個(gè)全局的訪問點(diǎn)來獲得這個(gè)"唯一"的對(duì)象
  public static LazySigleton getInstance(){      
       if(instatnce == null){               //1:讀取instance的值
          instatnce = new LazySigleton();   //2: 實(shí)例化instance   
        }            
        return instatnce;        
    }
}

懶漢比較懶,只有當(dāng)調(diào)用 getInstance 的時(shí)候,才會(huì)去初始化這個(gè)單例。

四、懶漢非線程安全原因(兩點(diǎn))

對(duì)于以上代碼注釋部分,如果此時(shí)有兩個(gè)線程,線程甲執(zhí)行到 1 處讀取了 instance 為 null,然后 cpu 就被線程乙搶去了,此時(shí)線程甲還沒有對(duì) instance 進(jìn)行實(shí)例化。因此,線程乙讀取 instance 時(shí)仍然為 null,于是它對(duì) instance 進(jìn)行實(shí)例化了。然后,cpu 時(shí)間片輪到線程甲。此時(shí),線程甲已經(jīng)讀取了 instance 的值并且認(rèn)為它為 null,再次對(duì) instance 進(jìn)行實(shí)例化。所以,線程甲和線程乙返回的不是同一個(gè)實(shí)例。

如何解決呢?

  1. 在方法前面加 synchronized 修飾。這樣肯定不會(huì)再有線程安全問題。
public class LazySigleton(){
    private static LazySigleton instatnce=null;
    private LazySigleton(){}
    public static synchronized LazySigleton getInstance(){      
      if(instatnce == null){        
         instatnce = new LazySigleton();      
       }            
       return instatnce;        
     }
}

這種解決方式有個(gè)問題:假如有 100 個(gè)線程同時(shí)執(zhí)行,那么每次去執(zhí)行 getInstance 方法時(shí)都要先獲得鎖再去執(zhí)行方法體。如果沒有鎖就要等待,耗時(shí)長,像是變成了串行處理。
特點(diǎn):
性能不高,同步范圍太大。在實(shí)例化后,獲取實(shí)例仍然是同步的,效率太低,需要縮小同步的范圍。

  1. 加同步代碼塊,減少鎖的顆粒大小。判斷 instance 是否為 null 是讀的操作,不存在線程安全問題。因此,只需要對(duì)創(chuàng)建實(shí)例的代碼進(jìn)行同步代碼塊的處理,也就是所謂的對(duì)可能出現(xiàn)線程安全的代碼進(jìn)行同步代碼塊的處理。
public class LazySigleton(){
    private static LazySigleton instatnce=null;
    private LazySigleton(){}
    public static LazySigleton getInstance(){      
         if(instatnce == null){ 
            synchronized (LazySigleton.class){       
               instatnce = new LazySigleton();      
            }  
          }          
          return instatnce;        
      }
}

這樣處理就沒有問題了嗎?同樣的原理,線程甲讀取 instance 值為 null,此時(shí) cpu 被線程乙搶去了,線程乙判斷 instance 值也為 null,于是它開始執(zhí)行同步代碼塊,對(duì) instance 進(jìn)行實(shí)例化。此時(shí),線程甲獲得 cpu,由于線程甲之前已經(jīng)判斷 instance 值為 null,于是開始執(zhí)行它后面的同步代碼塊。它也會(huì)去對(duì) instance 進(jìn)行實(shí)例化。這樣就導(dǎo)致了還是會(huì)創(chuàng)建兩個(gè)不一樣的實(shí)例。
特點(diǎn):
縮小同步范圍,來提高性能,但是仍然存在多次執(zhí)行instance=new Singleton()的可能,由此引出 double check。

如何解決上面的問題?

很簡單,在同步代碼塊中 instance 實(shí)例化之前進(jìn)行判斷,如果 instance 為 null,才對(duì)其進(jìn)行實(shí)例化。這樣,就能保證 instance 只會(huì)實(shí)例化一次了。也就是所謂的雙重檢查加鎖機(jī)制。

再次分析上面的場景:
線程甲讀取 instance 值為 null,此時(shí) cpu 被線程乙搶去了,線程乙再來判斷 instance 值為 null。于是,它開始執(zhí)行同步代碼塊,對(duì) instance 進(jìn)行了實(shí)例化。這時(shí)線程甲獲得 cpu 執(zhí)行權(quán),當(dāng)線程甲去執(zhí)行同步代碼塊時(shí),它再去判斷 instance 的值,由于線程乙執(zhí)行完后已經(jīng)將這個(gè)共享資源 instance 實(shí)例化了,所以 instance 不再為 null,所以,線程甲就不會(huì)再次實(shí)行實(shí)例化代碼了。

public class LazySigleton() {
    private LazySigleton(){}
    private static LazySigleton instatnce=null;
    public static LazySigleton getInstance(){
        if(instatnce== null) {
            synchronized (LazySigleton.class){
                if (instatnce == null){
                    instatnce = new LazySigleton();
                }
            }
        }
        return instatnce;
    }
}

雙重檢查加鎖并不表示一定沒有線程安全問題了。因?yàn)椋?a href="http://www.lxweimin.com/p/e657743298ef" target="_blank">Java內(nèi)存模型(JVM)并不限制處理器重排序。instatnce = new LazySigleton()并不是原子語句,其實(shí)可以分為下面的步驟:

  1. 申請一塊內(nèi)存空間;
  2. 在這塊空間里實(shí)例化對(duì)象;
  3. instatnce 的引用指向這塊空間地址(instatnce 指向分配的內(nèi)存空間后就不為 null 了)。
    【由此可理解,Java 中 new 操作是不具有原子性的

指令重排序存在的問題是:
對(duì)于以上步驟,指令重排序很有可能不是按上面【1、2、3】步驟依次執(zhí)行的。比如,先執(zhí)行 1 申請一塊內(nèi)存空間,然后執(zhí)行 3,instatnce 的引用去指向剛剛申請的內(nèi)存空間地址。那么,當(dāng)它再去執(zhí)行 2,判斷 instatnce 時(shí),由于 instatnce 已經(jīng)指向了某一地址,它就不會(huì)再為 null 了,因此,也就不會(huì)實(shí)例化對(duì)象了。這就是所謂的指令重排序安全問題。那么,如何解決這個(gè)問題呢?

加上 volatile 關(guān)鍵字,因?yàn)?volatile 可以禁止指令重排序。volatile 可以保證【1、2、3】的執(zhí)行順序,沒執(zhí)行完 1、2 就肯定不會(huì)執(zhí)行 3,也就是沒有執(zhí)行完 1、2,instance 一直為空。這樣就可以保證 3(instance 賦值操作)是最后一步完成,這樣就不會(huì)出現(xiàn) instance 在對(duì)象沒有初始化時(shí)就不為 null 的情況了。這樣也就實(shí)現(xiàn)了正確的單例模式。具體代碼如下:

public class LazySigleton() {
    private LazySigleton(){}
    private static volatile LazySigleton instatnce=null;
    public static LazySigleton getInstance(){
        if(instatnce== null) {
            synchronized (LazySigleton.class){
                if (instatnce == null){
                    instatnce = new LazySigleton();
                }
            }
        }
        return instatnce;
    }
}

附:
1??靜態(tài)內(nèi)部類懶漢模式

public class Singleton{
    private Singleton(){}
    public static Singleton getInstance(){
        return InstanceHolder.instance;
    }
    static class InstanceHolder{
        private static Singleton instance=new Singleton();
    }
}

靜態(tài)內(nèi)部類在沒有顯示調(diào)用的時(shí)候是不會(huì)進(jìn)行加載的,當(dāng)執(zhí)行了
return InstanceHolder.instance后才加載初始化,這樣就實(shí)現(xiàn)了正確的單例模式。
2??利用枚舉的特性在 JVM 層保證絕對(duì)的單例

class EnumSingleton {
      //私有構(gòu)造函數(shù),防止new對(duì)象
      private EnumSingleton() {}
      public static EnmuSingleton getInstance() {
          return Singleton.INSTANCE.getSingleton();
      }
      //JVM層保證絕對(duì)單例
      private enum Singleton {
          INSTANCE;
          private EnumSingleton singleton;
          Singleton() {
                singleton = new EnumSingleton();
          }
          public EnumSingleton getSingleton() {
                  return singleton;
          }
      } 
}

為啥枚舉的單例就完美了?枚舉本身是無法通過反射克隆反序列化等等把對(duì)象初始出來的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,202評(píng)論 3 426
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,297評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,688評(píng)論 1 327
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,875評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,438評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,183評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,384評(píng)論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,612評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評(píng)論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,093評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,330評(píng)論 2 377