深入 Java 單例模式

image

在 GoF 的23種設(shè)計(jì)模式中女坑,單例模式是比較簡(jiǎn)單的一種填具。然而,有時(shí)候越是簡(jiǎn)單的東西越容易出現(xiàn)問(wèn)題匆骗。下面就單例設(shè)計(jì)模式詳細(xì)的探討一下劳景。

所謂單例模式,簡(jiǎn)單來(lái)說(shuō)碉就,就是在整個(gè)應(yīng)用中保證只有一個(gè)類(lèi)的實(shí)例存在盟广。就像是 Java Web 中的 application,也就是提供了一個(gè)全局變量瓮钥,用處相當(dāng)廣泛筋量,比如:保存全局?jǐn)?shù)據(jù),實(shí)現(xiàn)全局性的操作等碉熄。

最簡(jiǎn)單的實(shí)現(xiàn)

首先桨武,能夠想到的最簡(jiǎn)單的實(shí)現(xiàn)是,把類(lèi)的構(gòu)造函數(shù)寫(xiě)成private的锈津,從而保證別的類(lèi)不能實(shí)例化此類(lèi)呀酸,然后在類(lèi)中提供一個(gè)靜態(tài)的實(shí)例并能夠返回給使用者。這樣琼梆,使用者就可以通過(guò)這個(gè)引用使用到這個(gè)類(lèi)的實(shí)例了性誉。

public class SingletonClass {

  private static final SingletonClass instance = new SingletonClass();

  private SingletonClass() {}

  public static SingletonClass getInstance() {
    return instance;
  }  
}

如上例,外部使用者如果需要使用SingletonClass的實(shí)例茎杂,只能通過(guò)getInstance()方法错览,并且它的構(gòu)造方法是private的,這樣就保證了只能有一個(gè)對(duì)象存在蛉顽。

性能優(yōu)化

Lazy Loaded

上面的代碼雖然簡(jiǎn)單蝗砾,但是有一個(gè)問(wèn)題——無(wú)論這個(gè)類(lèi)是否被使用先较,都會(huì)創(chuàng)建一個(gè) instance 對(duì)象携冤。如果這個(gè)創(chuàng)建過(guò)程很耗時(shí),比如需要連接 10000 次數(shù)據(jù)庫(kù)(夸張了…:-))闲勺,并且這個(gè)類(lèi)還并不一定會(huì)被使用曾棕,那么這個(gè)創(chuàng)建過(guò)程就是無(wú)用的。怎么辦呢菜循?為了解決這個(gè)問(wèn)題翘地,我們想到了新的解決方案:

public class SingletonClass {

  private static SingletonClass instance = null;

  private SingletonClass() {}

  public static SingletonClass getInstance() {
    if(instance == null) {
      instance = new SingletonClass();
    }
    return instance;
  }
}

代碼的變化有兩處 — 首先,把 instance 初始化為 null,直到第一次使用的時(shí)候通過(guò)判斷是否為 null 來(lái)創(chuàng)建對(duì)象衙耕。因?yàn)閯?chuàng)建過(guò)程不在聲明處昧穿,所以那個(gè)final的修飾必須去掉。

我們來(lái)想象一下這個(gè)過(guò)程橙喘。要使用 SingletonClass时鸵,調(diào)用 getInstance() 方法。第一次的時(shí)候發(fā)現(xiàn) instance 是 null厅瞎,然后就新建一個(gè)對(duì)象饰潜,返回出去;第二次再使用的時(shí)候和簸,因?yàn)檫@個(gè) instance 是 static 的彭雾,所以已經(jīng)不是 null 了,因此不會(huì)再創(chuàng)建對(duì)象锁保,直接將其返回薯酝。

這個(gè)過(guò)程就稱為Lazy Loaded,也就是遲加載 — 直到使用的時(shí)候才進(jìn)行加載身诺。

同步

上面的代碼很清楚蜜托,也很簡(jiǎn)單。然而就像那句名言:“80% 的錯(cuò)誤都是由 20% 代碼優(yōu)化引起的”霉赡。單線程下橄务,這段代碼沒(méi)有什么問(wèn)題,可是如果是多線程穴亏,麻煩就來(lái)了蜂挪。我們來(lái)分析一下:

線程 A 希望使用 SingletonClass,調(diào)用 getInstance() 方法嗓化。因?yàn)槭堑谝淮握{(diào)用棠涮,A 就發(fā)現(xiàn) instance 是 null 的,于是它開(kāi)始創(chuàng)建實(shí)例刺覆,就在這個(gè)時(shí)候严肪,CPU 發(fā)生時(shí)間片切換,線程 B 開(kāi)始執(zhí)行谦屑,它要使用 SingletonClass驳糯,調(diào)用 getInstance() 方法,同樣檢測(cè)到 instance 是 null — 注意氢橙,這是在 A 檢測(cè)完之后切換的酝枢,也就是說(shuō)A并沒(méi)有來(lái)得及創(chuàng)建對(duì)象——因此 B 開(kāi)始創(chuàng)建。B 創(chuàng)建完成后悍手,切換到 A 繼續(xù)執(zhí)行帘睦,因?yàn)樗呀?jīng)檢測(cè)完了袍患,所以 A 不會(huì)再檢測(cè)一遍,它會(huì)直接創(chuàng)建對(duì)象竣付。這樣诡延,線程 A 和 B 各自擁有一個(gè) SingletonClass 的對(duì)象 — 單例失敗古胆!解決的方法也很簡(jiǎn)單孕暇,那就是加鎖:

public class SingletonClass {

  private static SingletonClass instance = null;

  private SingletonClass() {}

  public synchronized static SingletonClass getInstance() {
    if(instance == null) {
      instance = new SingletonClass();
    }
    return instance;
  }
}

需要在 getInstance() 加上同步鎖,一個(gè)線程必須等待另外一個(gè)線程創(chuàng)建完成后才能使用這個(gè)方法赤兴,這就保證了單例的唯一性妖滔。

性能問(wèn)題

上面的代碼又是很清楚很簡(jiǎn)單的,然而桶良,簡(jiǎn)單的東西往往不夠理想座舍。這段代碼毫無(wú)疑問(wèn)存在性能的問(wèn)題 — synchronized修飾的同步塊可是要比一般的代碼段慢上幾倍的!如果存在很多次getInstance()的調(diào)用陨帆,那性能問(wèn)題就不得不考慮了曲秉!

讓我們來(lái)分析一下,究竟是整個(gè)方法都必須加鎖疲牵,還是僅僅其中某一句加鎖就足夠了承二?我們?yōu)槭裁匆渔i呢?分析一下出現(xiàn)Lazy Loaded的那種情形的原因纲爸。原因就是檢測(cè)null的操作和創(chuàng)建對(duì)象的操作分離了亥鸠。如果這兩個(gè)操作能夠原子地進(jìn)行,那么單例就已經(jīng)保證了识啦。于是负蚊,我們開(kāi)始修改代碼:

public class SingletonClass {

  private static SingletonClass instance = null;

  private SingletonClass() {}

  public static SingletonClass getInstance() {
    synchronized (SingletonClass.class) {
      if(instance == null) {
        instance = new SingletonClass();
      }
    }     
    return instance;
  }
}

首先去掉 getInstance() 的同步操作,然后把同步鎖加載if語(yǔ)句上颓哮。但是這樣的修改起不到任何作用:因?yàn)槊看握{(diào)用 getInstance() 的時(shí)候必然要同步家妆,性能問(wèn)題還是存在。如果我們事先判斷一下是不是為 null 再去同步呢冕茅?

public class SingletonClass {

  private static SingletonClass instance = null;

  private SingletonClass() {}

  public static SingletonClass getInstance() {
    if (instance == null) {
      synchronized (SingletonClass.class) {
        if (instance == null) {
          instance = new SingletonClass();
        }
      }
    }
    return instance;
  }
}

還有問(wèn)題嗎伤极?首先判斷 instance 是不是為 null,如果為 null姨伤,加鎖初始化哨坪;如果不為 null,直接返回 instance姜挺。這就是double-checked locking設(shè)計(jì)實(shí)現(xiàn)單例模式齿税。到此為止彼硫,一切都很完美炊豪。我們用一種很聰明的方式實(shí)現(xiàn)了單例模式凌箕。

從源頭檢查

代碼。編譯原理里面有一個(gè)很重要的內(nèi)容是編譯器優(yōu)化词渤。所謂編譯器優(yōu)化是指牵舱,在不改變?cè)瓉?lái)語(yǔ)義的情況下,通過(guò)調(diào)整語(yǔ)句順序缺虐,來(lái)讓程序運(yùn)行的更快芜壁。這個(gè)過(guò)程稱為reorder

要知道高氮,JVM 只是一個(gè)標(biāo)準(zhǔn)慧妄,并不是實(shí)現(xiàn)。JVM 中并沒(méi)有規(guī)定有關(guān)編譯器優(yōu)化的內(nèi)容剪芍,也就是說(shuō)塞淹,JVM 實(shí)現(xiàn)可以自由的進(jìn)行編譯器優(yōu)化。

下面來(lái)想一下罪裹,創(chuàng)建一個(gè)變量需要哪些步驟呢饱普?一個(gè)是申請(qǐng)一塊內(nèi)存,調(diào)用構(gòu)造方法進(jìn)行初始化操作状共,另一個(gè)是分配一個(gè)指針指向這塊內(nèi)存套耕。這兩個(gè)操作誰(shuí)在前誰(shuí)在后呢?JVM 規(guī)范并沒(méi)有規(guī)定峡继。那么就存在這么一種情況冯袍,JVM 是先開(kāi)辟出一塊內(nèi)存,然后把指針指向這塊內(nèi)存碾牌,最后調(diào)用構(gòu)造方法進(jìn)行初始化颠猴。

下面我們來(lái)考慮這么一種情況:線程 A 開(kāi)始創(chuàng)建 SingletonClass 的實(shí)例,此時(shí)線程 B 調(diào)用了 getInstance() 方法小染,首先判斷 instance 是否為 null翘瓮。按照我們上面所說(shuō)的內(nèi)存模型,A 已經(jīng)把 instance 指向了那塊內(nèi)存裤翩,只是還沒(méi)有調(diào)用構(gòu)造方法资盅,因此B檢測(cè)到 instance 不為 null,于是直接把 instance 返回了 — 問(wèn)題出現(xiàn)了踊赠,盡管 instance 不為 null呵扛,但它并沒(méi)有構(gòu)造完成,就像一套房子已經(jīng)給了你鑰匙筐带,但你并不能住進(jìn)去今穿,因?yàn)槔锩孢€沒(méi)有收拾。此時(shí)伦籍,如果 B 在 A 將 instance 構(gòu)造完成之前就是用了這個(gè)實(shí)例蓝晒,程序就會(huì)出現(xiàn)錯(cuò)誤了腮出!于是,我們想到了下面的代碼:

public class SingletonClass {

  private static SingletonClass instance = null;

  private SingletonClass() {}

  public static SingletonClass getInstance() {
    if (instance == null) {
      SingletonClass sc;
      synchronized (SingletonClass.class) {
        sc = instance;
        if (sc == null) {
          synchronized (SingletonClass.class) {
            if(sc == null) {
              sc = new SingletonClass();
            }
          }
          instance = sc;
        }
      }
    }
    return instance;
  }
}

我們?cè)诘谝粋€(gè)同步塊里面創(chuàng)建一個(gè)臨時(shí)變量芝薇,然后使用這個(gè)臨時(shí)變量進(jìn)行對(duì)象的創(chuàng)建胚嘲,并且在最后把instance指針臨時(shí)變量的內(nèi)存空間。寫(xiě)出這種代碼基于以下思想洛二,即synchronized會(huì)起到一個(gè)代碼屏蔽的作用馋劈,同步塊里面的代碼和外部的代碼沒(méi)有聯(lián)系。因此晾嘶,在外部的同步塊里面對(duì)臨時(shí)變量sc進(jìn)行操作并不影響 instance妓雾,所以外部類(lèi)在instance = sc;之前檢測(cè) instance 的時(shí)候,結(jié)果 instance 依然是 null垒迂。

不過(guò)君珠,這種想法完全是錯(cuò)誤的!同步塊的釋放保證在此之前 — 也就是同步塊里面 — 的操作必須完成娇斑,但是并不保證同步塊之后的操作不能因編譯器優(yōu)化而調(diào)換到同步塊結(jié)束之前進(jìn)行策添。因此,編譯器完全可以把instance = sc; 這句移到內(nèi)部同步塊里面執(zhí)行毫缆。這樣唯竹,程序又是錯(cuò)誤的了!

解決方案

說(shuō)了這么多苦丁,難道單例沒(méi)有辦法在 Java 中實(shí)現(xiàn)嗎浸颓?其實(shí)不然!
在 JDK5 之后旺拉,Java 使用了新的內(nèi)存模型产上。volatile關(guān)鍵字有了明確的語(yǔ)義 — 在 JDK1.5 之前,volatile是個(gè)關(guān)鍵字蛾狗,但是并沒(méi)有明確的規(guī)定其用途 — 被 volatile 修飾的寫(xiě)變量不能和之前的讀寫(xiě)代碼調(diào)整晋涣,讀變量不能和之后的讀寫(xiě)代碼調(diào)整!因此沉桌,只要我們簡(jiǎn)單的把 instance 加上 volatile 關(guān)鍵字就可以了谢鹊。

public class SingletonClass {

  private volatile static SingletonClass instance = null;

  private SingletonClass() {}

  public static SingletonClass getInstance() {
    if (instance == null) {
      synchronized (SingletonClass.class) {
        if(instance == null) {
          instance = new SingletonClass();
        }
      }
    }
    return instance;
  }
}
  • But,這只是 JDK1.5 之后的 Java 的解決方案留凭,那之前版本呢佃扼?其實(shí),還有另外的一種解決方案蔼夜,并不會(huì)受到 Java 版本的影響:
public class SingletonClass {

  private SingletonClass() {}

  private static class SingletonClassInstance {
    private static final SingletonClass instance = new SingletonClass();
  }

  public static SingletonClass getInstance() {
    return SingletonClassInstance.instance;
  }
}

在這一版本的單例模式實(shí)現(xiàn)代碼中兼耀,我們使用了 Java 的靜態(tài)內(nèi)部類(lèi)。這一技術(shù)是被 JVM 明確說(shuō)明了的,因此不存在任何二義性瘤运。在這段代碼中窍霞,因?yàn)?SingletonClass 沒(méi)有 static 的屬性,因此并不會(huì)被初始化尽超。直到調(diào)用 getInstance() 的時(shí)候,會(huì)首先加載 SingletonClassInstance 類(lèi)梧躺,這個(gè)類(lèi)有一個(gè) static 的 SingletonClass 實(shí)例似谁,因此需要調(diào)用 SingletonClass 的構(gòu)造方法,然后 getInstance() 將把這個(gè)內(nèi)部類(lèi)的 instance 返回給使用者掠哥。由于這個(gè) instance 是 static 的巩踏,因此并不會(huì)構(gòu)造多次。

由于 SingletonClassInstance 是私有靜態(tài)內(nèi)部類(lèi)续搀,所以不會(huì)被其他類(lèi)知道塞琼,同樣, static 語(yǔ)義也要求不會(huì)有多個(gè)實(shí)例存在禁舷。并且彪杉,JSL 規(guī)范定義,類(lèi)的構(gòu)造必須是原子性的牵咙,非并發(fā)的派近,因此不需要加同步塊。同樣洁桌,由于這個(gè)構(gòu)造是并發(fā)的渴丸,所以 getInstance() 也并不需要加同步。

至此另凌,我們完整的了解了單例模式在 Java 語(yǔ)言中的時(shí)候谱轨,提出了兩種解決方案。個(gè)人偏向于第二種,并且 Effiective Java 也推薦的這種方式。

【轉(zhuǎn)載】原文出處:【豆子空間

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末祈争,一起剝皮案震驚了整個(gè)濱河市饺汹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌逗物,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異雀瓢,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)玉掸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)刃麸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人司浪,你說(shuō)我怎么就攤上這事泊业“颜樱” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵吁伺,是天一觀的道長(zhǎng)饮睬。 經(jīng)常有香客問(wèn)我,道長(zhǎng)篮奄,這世上最難降的妖魔是什么捆愁? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮窟却,結(jié)果婚禮上昼丑,老公的妹妹穿的比我還像新娘。我一直安慰自己夸赫,他們只是感情好菩帝,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著茬腿,像睡著了一般呼奢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上切平,一...
    開(kāi)封第一講書(shū)人閱讀 49,785評(píng)論 1 290
  • 那天控妻,我揣著相機(jī)與錄音,去河邊找鬼揭绑。 笑死弓候,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的他匪。 我是一名探鬼主播菇存,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼邦蜜!你這毒婦竟也來(lái)了依鸥?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤悼沈,失蹤者是張志新(化名)和其女友劉穎贱迟,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體絮供,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡衣吠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了壤靶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缚俏。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出忧换,到底是詐尸還是另有隱情恬惯,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布亚茬,位于F島的核電站酪耳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏刹缝。R本人自食惡果不足惜碗暗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望赞草。 院中可真熱鬧讹堤,春花似錦吆鹤、人聲如沸厨疙。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)沾凄。三九已至,卻和暖如春知允,著一層夾襖步出監(jiān)牢的瞬間撒蟀,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工温鸽, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留保屯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓涤垫,卻偏偏與公主長(zhǎng)得像姑尺,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蝠猬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348

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