應(yīng)用最廣的設(shè)計(jì)模式-單例模式

模式介紹

單例模式是應(yīng)用最廣泛的模式之一颜说。

單例模式是為了確保一個(gè)類(lèi)在整個(gè)項(xiàng)目中只有一個(gè)實(shí)例對(duì)象。

單例模式最大的優(yōu)勢(shì)就是可以避免資源的浪費(fèi)啸澡。

比如訪問(wèn)IO和數(shù)據(jù)庫(kù)等資源時(shí)就應(yīng)考慮使用單例模式飞崖。

模式特點(diǎn)

  1. 構(gòu)造方法私有化,使用private來(lái)修飾;

  2. 確保對(duì)象有且只有一個(gè)铲咨,尤其是在多線程的環(huán)境下躲胳;

  3. 通過(guò)靜態(tài)方法或枚舉返回已經(jīng)實(shí)例化好的對(duì)象。

模式示例

實(shí)現(xiàn)單例模式的方式有很多纤勒,不過(guò)核心是不變的坯苹,都要嚴(yán)格遵循單例模式的特點(diǎn)。

下面我們來(lái)介紹實(shí)現(xiàn)單例的方式:

1. 餓漢式

public class 餓漢式 {
    //自行實(shí)例化對(duì)象
    private static final 餓漢式 ourInstance = new 餓漢式();
    //通過(guò)靜態(tài)方法返回對(duì)象
    public static 餓漢式 getInstance() {
        return ourInstance;
    }
    //構(gòu)造方法私有化摇天,不能通過(guò)new來(lái)創(chuàng)建對(duì)象
    private 餓漢式() {
    }
}

值得一提的是粹湃,AndroidStudio在創(chuàng)建類(lèi)時(shí)指定該類(lèi)為單例的時(shí)候,默認(rèn)就是使用餓漢式:


餓漢式寫(xiě)起來(lái)非常簡(jiǎn)單泉坐、快捷再芋,但是缺點(diǎn)也顯而易見(jiàn):

在類(lèi)初始化的時(shí)候,對(duì)象就已經(jīng)創(chuàng)建好了坚冀。

如果說(shuō)我們沒(méi)有用到該類(lèi)济赎,就會(huì)造成資源的浪費(fèi)。

2. 懶漢式

public class 最簡(jiǎn)單的懶漢式 {
   //全項(xiàng)目唯一的對(duì)象
   private static 最簡(jiǎn)單的懶漢式 ourInstance;

   //構(gòu)造方法私有化
   private 最簡(jiǎn)單的懶漢式() {

   }

   //通過(guò)靜態(tài)方法來(lái)返回對(duì)象
   public static 最簡(jiǎn)單的懶漢式 getInstance() {
       //在調(diào)用該方法時(shí)進(jìn)行判空记某,在對(duì)象為null時(shí)創(chuàng)建對(duì)象
       if (ourInstance == null) {
           ourInstance = new 最簡(jiǎn)單的懶漢式();
       }
       return ourInstance;
   }
}

這就是單例中懶漢式的最基本寫(xiě)法司训。

比起餓漢式,最大的優(yōu)勢(shì)就是不會(huì)造成資源的浪費(fèi)液南。因?yàn)橹挥性谟玫綍r(shí)壳猜,才會(huì)進(jìn)行對(duì)象的實(shí)例化。

但是就上面的寫(xiě)法而言滑凉,還存在一個(gè)很致命的問(wèn)題:

在多線程同時(shí)調(diào)用時(shí)统扳,會(huì)出現(xiàn)多個(gè)實(shí)例對(duì)象的情況

Demo里有對(duì)應(yīng)的測(cè)試代碼畅姊,出現(xiàn)的概率很小咒钟,但是確實(shí)會(huì)出現(xiàn)。

解決這個(gè)問(wèn)題的方式也很簡(jiǎn)單若未,為靜態(tài)方法添加同步鎖:

//通過(guò)靜態(tài)方法來(lái)返回對(duì)象
public static synchronized 同步鎖的懶漢式 getInstance() {
     //在調(diào)用該方法時(shí)進(jìn)行判空朱嘴,在對(duì)象為null時(shí)創(chuàng)建對(duì)象
        if (ourInstance == null) {
            ourInstance = new 同步鎖的懶漢式();
        }
         return ourInstance;
}

synchronized就是同步鎖的關(guān)鍵字,加上該關(guān)鍵字粗合,代表著該方法同時(shí)只能在唯一的一個(gè)線程中運(yùn)行萍嬉。

比如當(dāng)10個(gè)線程去調(diào)用同步鎖的懶漢式.getInstance()時(shí),只有當(dāng)?shù)?個(gè)線程完成訪問(wèn)時(shí)隙疚,第2個(gè)線程才會(huì)開(kāi)始執(zhí)行該方法壤追。當(dāng)?shù)?個(gè)線程訪問(wèn)完成后,單例對(duì)象就已經(jīng)創(chuàng)建完成供屉,所以第2個(gè)線程就會(huì)直接返回該對(duì)象行冰,不會(huì)再去創(chuàng)建捅厂,這就保證了線程安全。

這樣確實(shí)解決了我們所說(shuō)的線程安全的問(wèn)題资柔,但是這種做法明顯是低效率的:

我們的目的是保證項(xiàng)目中有且只有一個(gè)對(duì)象焙贷,上述代碼確實(shí)實(shí)現(xiàn)了這個(gè)目的。

但是當(dāng)對(duì)象創(chuàng)建成功后贿堰,我們希望多線程訪問(wèn)的時(shí)候應(yīng)該是異步高效辙芍、同時(shí)執(zhí)行的的,而不是像上面那樣隊(duì)列式的羹与,我要等你用完我才能用故硅。所以就有了雙重校驗(yàn)鎖的懶漢式:

public static 同步鎖的懶漢式 getInstance() {
    if (ourInstance == null) {
        synchronized (new Object()) {
            if (ourInstance == null) {
                ourInstance = new 同步鎖的懶漢式()
            }
        }
    }
    return ourInstance;
}

這種寫(xiě)法可以完美解決多線程效率低下的問(wèn)題,那么到底是如何解決的纵搁?

雙重校驗(yàn)鎖指的是會(huì)進(jìn)行兩次判空操作:

ourInstance == null

一次在同步鎖外吃衅,一次在同步鎖內(nèi)。

有的看官就有疑問(wèn)了:兩次判空腾誉?

首先是synchronized關(guān)鍵字徘层,我們刪除了方法的同步鎖,將其移動(dòng)到了方法內(nèi)部利职,對(duì)ourInstance = new 同步鎖的懶漢式()單獨(dú)加鎖趣效。

這就代表著我們這個(gè)方法本身已經(jīng)不是線程安全了,會(huì)有多個(gè)線程同時(shí)訪問(wèn)外層的if猪贪。如果同步鎖內(nèi)部沒(méi)有判空跷敬,就會(huì)有多個(gè)線程等待對(duì)象創(chuàng)建,就會(huì)生成多個(gè)實(shí)例對(duì)象热押。

所以雙重校驗(yàn)鎖的每一步都非常關(guān)鍵西傀,必不可少。

雙重校驗(yàn)鎖的寫(xiě)法主要是為了在多線程創(chuàng)建對(duì)象時(shí)桶癣,用同步鎖來(lái)保證對(duì)象的唯一拥褂。當(dāng)對(duì)象創(chuàng)建完成后,同步鎖外層的判空操作就不成立了鬼廓,那么會(huì)直接返回對(duì)象肿仑,整個(gè)方法就與同步鎖無(wú)關(guān)致盟,多線程訪問(wèn)時(shí)也就不需要等待了碎税。

雙重校驗(yàn)鎖懶漢式,看起來(lái)已經(jīng)非常完美了馏锡!

但是雷蹂,很遺憾。

因?yàn)镴VM存在指令重排的優(yōu)化杯道,又會(huì)產(chǎn)生新的問(wèn)題匪煌。

指令重排是JVM為了提高程序運(yùn)行效率责蝠。

JVM規(guī)范規(guī)定,指令重排序可以在不影響單線程程序執(zhí)行結(jié)果的情況下改變代碼執(zhí)行順序萎庭。

該處會(huì)產(chǎn)生指令重排的代碼是

ourInstance = new 同步鎖的懶漢式();

這句代碼在JVM看來(lái)霜医,主要是做了以下三件事情:
(1)給ourInstance分配內(nèi)存;

(2)調(diào)用構(gòu)造方法創(chuàng)建對(duì)象驳规,對(duì)對(duì)象進(jìn)行初始化肴敛;

(3)將ourInstance對(duì)象指向JVM分配的內(nèi)存空間(此步完成之后,ourInstance就是非null了)吗购。

因?yàn)镴VM存在指令重排医男,所以在不影響最終結(jié)果的情況下,JVM會(huì)選擇性能最優(yōu)的的順序執(zhí)行:

也就是說(shuō)捻勉,上面三件事情镀梭,執(zhí)行的順序可能是1-2-3,也有可能是1-3-2踱启。

1-2-3报账,1-3-2,有區(qū)別嗎埠偿?

在結(jié)果上來(lái)看笙什,沒(méi)有任何區(qū)別。

但是在多線程的情況下胚想,是有風(fēng)險(xiǎn)的:

假設(shè)線程x的執(zhí)行順序是1-3-2琐凭,當(dāng)3執(zhí)行完成時(shí),ourInstance就已經(jīng)不為空了浊服,但是2還沒(méi)有執(zhí)行完成時(shí)统屈,線程y介入了。此時(shí)線程y會(huì)發(fā)現(xiàn)ourInstance已經(jīng)不為null了牙躺,但是其實(shí)ourInstance的初始化工作并未完成愁憔,這樣很明顯就會(huì)產(chǎn)生異常。

解決方法也非常簡(jiǎn)單孽拷,利用volatile關(guān)鍵字即可:

public class 完美的懶漢式 {
    //全項(xiàng)目唯一的對(duì)象
    //volatile關(guān)鍵字吨掌,禁止指令重排
    private volatile  static 完美的懶漢式 ourInstance;

    //構(gòu)造方法私有化
    private 完美的懶漢式() {

    }

    //通過(guò)靜態(tài)方法來(lái)返回對(duì)象
    public static 完美的懶漢式 getInstance() {
        //在調(diào)用該方法時(shí)進(jìn)行判空,在對(duì)象為null時(shí)創(chuàng)建對(duì)象
        if (ourInstance == null) {
            synchronized (new Object()) {
                if (ourInstance == null) {
                    ourInstance = new 完美的懶漢式();
                }
            }
        }
        return ourInstance;
    }

上述代碼就是一個(gè)完美的懶漢式了脓恕,利用volatile關(guān)鍵字來(lái)禁止JVM的指令重排膜宋。

3. 枚舉(Enum)

 public enum 枚舉單例 {

    INSTANCE;
    
    public String getUrl(){
        return "http://www.baidu.com";
    }
}

使用起來(lái)也非常簡(jiǎn)單:

String url = 枚舉單例.INSTANCE.getUrl();

簡(jiǎn)直完美啊炼幔!簡(jiǎn)單易用秋茫,代碼清晰!

總結(jié)

簡(jiǎn)單回顧一下:

單例模式是保證了一個(gè)類(lèi)在一個(gè)項(xiàng)目中有且只有一個(gè)實(shí)例對(duì)象乃秀。

這樣做的目的是為了節(jié)省內(nèi)存的開(kāi)支肛著。

單例模式的寫(xiě)法主要有:

  • 項(xiàng)目初始化時(shí)就創(chuàng)建好的餓漢式圆兵;
  • 在第一次使用時(shí)才進(jìn)行創(chuàng)建、但要注意線程安全的懶漢式枢贿;
  • 使用非常簡(jiǎn)單的枚舉殉农。

感謝

Jark's Blog-如何正確地寫(xiě)出單例模式

《Android源碼設(shè)計(jì)模式解析與實(shí)戰(zhàn)》 何紅輝、關(guān)愛(ài)民 著

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末局荚,一起剝皮案震驚了整個(gè)濱河市统抬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌危队,老刑警劉巖聪建,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異茫陆,居然都是意外死亡金麸,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)簿盅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)挥下,“玉大人,你說(shuō)我怎么就攤上這事桨醋∨镂粒” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵喜最,是天一觀的道長(zhǎng)偎蘸。 經(jīng)常有香客問(wèn)我,道長(zhǎng)瞬内,這世上最難降的妖魔是什么迷雪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮虫蝶,結(jié)果婚禮上章咧,老公的妹妹穿的比我還像新娘。我一直安慰自己能真,他們只是感情好赁严,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著粉铐,像睡著了一般疼约。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上秦躯,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天忆谓,我揣著相機(jī)與錄音,去河邊找鬼踱承。 笑死倡缠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的茎活。 我是一名探鬼主播昙沦,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼载荔!你這毒婦竟也來(lái)了盾饮?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤懒熙,失蹤者是張志新(化名)和其女友劉穎丘损,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體工扎,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡徘钥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肢娘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呈础。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖橱健,靈堂內(nèi)的尸體忽然破棺而出而钞,到底是詐尸還是另有隱情,我是刑警寧澤拘荡,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布臼节,位于F島的核電站,受9級(jí)特大地震影響珊皿,放射性物質(zhì)發(fā)生泄漏官疲。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一亮隙、第九天 我趴在偏房一處隱蔽的房頂上張望途凫。 院中可真熱鬧,春花似錦溢吻、人聲如沸维费。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)犀盟。三九已至,卻和暖如春蝇狼,著一層夾襖步出監(jiān)牢的瞬間阅畴,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工迅耘, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贱枣,地道東北人监署。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像纽哥,于是被迫代替她去往敵國(guó)和親钠乏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348