一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

Hi,我們?cè)賮?lái)聊一聊 Java 的單例

 釋皇天 2017-08-31


來(lái)源:張新強(qiáng),

www./archives/521

如有好文章投稿,請(qǐng)點(diǎn)擊 → 這里了解詳情


1. 前言


單例(Singleton)應(yīng)該是開發(fā)者們最熟悉的設(shè)計(jì)模式了,并且好像也是最容易實(shí)現(xiàn)的——基本上每個(gè)開發(fā)者都能夠隨手寫出——但是,真的是這樣嗎?


作為一個(gè)Java開發(fā)者,也許你覺得自己對(duì)單例模式的了解已經(jīng)足夠多了。我并不想危言聳聽說(shuō)一定還有你不知道的——畢竟我自己的了解也的確有限,但究竟你自己了解的程度到底怎樣呢?往下看,我們一起來(lái)聊聊看~


2. 什么是單例?


單例對(duì)象的類必須保證只有一個(gè)實(shí)例存在——這是維基百科上對(duì)單例的定義,這也可以作為對(duì)意圖實(shí)現(xiàn)單例模式的代碼進(jìn)行檢驗(yàn)的標(biāo)準(zhǔn)。


對(duì)單例的實(shí)現(xiàn)可以分為兩大類——懶漢式和餓漢式,他們的區(qū)別在于:


  • 懶漢式:指全局的單例實(shí)例在第一次被使用時(shí)構(gòu)建。

  • 餓漢式:指全局的單例實(shí)例在類裝載時(shí)構(gòu)建。


從它們的區(qū)別也能看出來(lái),日常我們使用的較多的應(yīng)該是懶漢式的單例,畢竟按需加載才能做到資源的最大化利用嘛~


3. 懶漢式單例


先來(lái)看一下懶漢式單例的實(shí)現(xiàn)方式。


3.1 簡(jiǎn)單版本


看最簡(jiǎn)單的寫法Version 1:


// Version 1

public class Single1 {

    private static Single1 instance;

    public static Single1 getInstance() {

        if (instance == null) {

            instance = new Single1();

        }

        return instance;

    }

}


或者再進(jìn)一步,把構(gòu)造器改為私有的,這樣能夠防止被外部的類調(diào)用。


// Version 1.1

public class Single1 {

    private static Single1 instance;

    private Single1() {}

    public static Single1 getInstance() {

        if (instance == null) {

            instance = new Single1();

        }

        return instance;

    }

}


我仿佛記得當(dāng)初學(xué)校的教科書就是這么教的?—— 每次獲取instance之前先進(jìn)行判斷,如果instance為空就new一個(gè)出來(lái),否則就直接返回已存在的instance。


這種寫法在大多數(shù)的時(shí)候也是沒問(wèn)題的。問(wèn)題在于,當(dāng)多線程工作的時(shí)候,如果有多個(gè)線程同時(shí)運(yùn)行到if (instance == null),都判斷為null,那么兩個(gè)線程就各自會(huì)創(chuàng)建一個(gè)實(shí)例——這樣一來(lái),就不是單例了。


3.2 synchronized版本


那既然可能會(huì)因?yàn)槎嗑€程導(dǎo)致問(wèn)題,那么加上一個(gè)同步鎖吧!


修改后的代碼如下,相對(duì)于Version1.1,只是在方法簽名上多加了一個(gè)synchronized:


// Version 2 

public class Single2 {

    private static Single2 instance;

    private Single2() {}

    public static synchronized Single2 getInstance() {

        if (instance == null) {

            instance = new Single2();

        }

        return instance;

    }

}


OK,加上synchronized關(guān)鍵字之后,getInstance方法就會(huì)鎖上了。如果有兩個(gè)線程(T1、T2)同時(shí)執(zhí)行到這個(gè)方法時(shí),會(huì)有其中一個(gè)線程T1獲得同步鎖,得以繼續(xù)執(zhí)行,而另一個(gè)線程T2則需要等待,當(dāng)?shù)赥1執(zhí)行完畢getInstance之后(完成了null判斷、對(duì)象創(chuàng)建、獲得返回值之后),T2線程才會(huì)執(zhí)行執(zhí)行?!赃@端代碼也就避免了Version1中,可能出現(xiàn)因?yàn)槎嗑€程導(dǎo)致多個(gè)實(shí)例的情況。


但是,這種寫法也有一個(gè)問(wèn)題:給gitInstance方法加鎖,雖然會(huì)避免了可能會(huì)出現(xiàn)的多個(gè)實(shí)例問(wèn)題,但是會(huì)強(qiáng)制除T1之外的所有線程等待,實(shí)際上會(huì)對(duì)程序的執(zhí)行效率造成負(fù)面影響。


3.3 雙重檢查(Double-Check)版本


Version2代碼相對(duì)于Version1d代碼的效率問(wèn)題,其實(shí)是為了解決1%幾率的問(wèn)題,而使用了一個(gè)100%出現(xiàn)的防護(hù)盾。那有一個(gè)優(yōu)化的思路,就是把100%出現(xiàn)的防護(hù)盾,也改為1%的幾率出現(xiàn),使之只出現(xiàn)在可能會(huì)導(dǎo)致多個(gè)實(shí)例出現(xiàn)的地方。


——有沒有這樣的方法呢?當(dāng)然是有的,改進(jìn)后的代碼Vsersion3如下:


// Version 3 

public class Single3 {

    private static Single3 instance;

    private Single3() {}

    public static Single3 getInstance() {

        if (instance == null) {

            synchronized (Single3.class) {

                if (instance == null) {

                    instance = new Single3();

                }

            }

        }

        return instance;

    }

}


這個(gè)版本的代碼看起來(lái)有點(diǎn)復(fù)雜,注意其中有兩次if (instance == null)的判斷,這個(gè)叫做『雙重檢查 Double-Check』。


  • 第一個(gè)if (instance == null),其實(shí)是為了解決Version2中的效率問(wèn)題,只有instance為null的時(shí)候,才進(jìn)入synchronized的代碼段——大大減少了幾率。


  • 第二個(gè)if (instance == null),則是跟Version2一樣,是為了防止可能出現(xiàn)多個(gè)實(shí)例的情況。


—— 這段代碼看起來(lái)已經(jīng)完美無(wú)瑕了。

……

……

……

—— 當(dāng)然,只是『看起來(lái)』,還是有小概率出現(xiàn)問(wèn)題的。


這弄清楚為什么這里可能出現(xiàn)問(wèn)題,首先,我們需要弄清楚幾個(gè)概念:原子操作、指令重排。


知識(shí)點(diǎn):什么是原子操作?


簡(jiǎn)單來(lái)說(shuō),原子操作(atomic)就是不可分割的操作,在計(jì)算機(jī)中,就是指不會(huì)因?yàn)榫€程調(diào)度被打斷的操作。


比如,簡(jiǎn)單的賦值是一個(gè)原子操作:


m = 6; // 這是個(gè)原子操作


假如m原先的值為0,那么對(duì)于這個(gè)操作,要么執(zhí)行成功m變成了6,要么是沒執(zhí)行m還是0,而不會(huì)出現(xiàn)諸如m=3這種中間態(tài)——即使是在并發(fā)的線程中。


而,聲明并賦值就不是一個(gè)原子操作:


int n = 6; // 這不是一個(gè)原子操作


對(duì)于這個(gè)語(yǔ)句,至少有兩個(gè)操作:


①聲明一個(gè)變量n

②給n賦值為6


——這樣就會(huì)有一個(gè)中間狀態(tài):變量n已經(jīng)被聲明了但是還沒有被賦值的狀態(tài)。

——這樣,在多線程中,由于線程執(zhí)行順序的不確定性,如果兩個(gè)線程都使用m,就可能會(huì)導(dǎo)致不穩(wěn)定的結(jié)果出現(xiàn)。


知識(shí)點(diǎn):什么是指令重排?


簡(jiǎn)單來(lái)說(shuō),就是計(jì)算機(jī)為了提高執(zhí)行效率,會(huì)做的一些優(yōu)化,在不影響最終結(jié)果的情況下,可能會(huì)對(duì)一些語(yǔ)句的執(zhí)行順序進(jìn)行調(diào)整。

比如,這一段代碼:


int a ;   // 語(yǔ)句1 

a = 8 ;   // 語(yǔ)句2

int b = 9 ;     // 語(yǔ)句3

int c = a + b ; // 語(yǔ)句4


正常來(lái)說(shuō),對(duì)于順序結(jié)構(gòu),執(zhí)行的順序是自上到下,也即1234。


但是,由于指令重排的原因,因?yàn)椴挥绊懽罱K的結(jié)果,所以,實(shí)際執(zhí)行的順序可能會(huì)變成3124或者1324。


由于語(yǔ)句3和4沒有原子性的問(wèn)題,語(yǔ)句3和語(yǔ)句4也可能會(huì)拆分成原子操作,再重排。


——也就是說(shuō),對(duì)于非原子性的操作,在不影響最終結(jié)果的情況下,其拆分成的原子操作可能會(huì)被重新排列執(zhí)行順序。


OK,了解了原子操作和指令重排的概念之后,我們?cè)倮^續(xù)看Version3代碼的問(wèn)題。


下面這段話直接從陳皓的文章(深入淺出單實(shí)例SINGLETON設(shè)計(jì)模式)中復(fù)制而來(lái):


主要在于singleton = new Singleton()這句,這并非是一個(gè)原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情。


1. 給 singleton 分配內(nèi)存


2. 調(diào)用 Singleton 的構(gòu)造函數(shù)來(lái)初始化成員變量,形成實(shí)例


3. 將singleton對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 singleton才是非 null 了)

但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說(shuō)上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時(shí) instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會(huì)直接返回 instance,然后使用,然后順理成章地報(bào)錯(cuò)。


再稍微解釋一下,就是說(shuō),由于有一個(gè)『instance已經(jīng)不為null但是仍沒有完成初始化』的中間狀態(tài),而這個(gè)時(shí)候,如果有其他線程剛好運(yùn)行到第一層if (instance == null)這里,這里讀取到的instance已經(jīng)不為null了,所以就直接把這個(gè)中間狀態(tài)的instance拿去用了,就會(huì)產(chǎn)生問(wèn)題。


這里的關(guān)鍵在于——線程T1對(duì)instance的寫操作沒有完成,線程T2就執(zhí)行了讀操作。


3.4 終極版本:volatile


對(duì)于Version3中可能出現(xiàn)的問(wèn)題(當(dāng)然這種概率已經(jīng)非常小了,但畢竟還是有的嘛~),解決方案是:只需要給instance的聲明加上volatile關(guān)鍵字即可,Version4版本:


// Version 4 

public class Single4 {

    private static volatile Single4 instance;

    private Single4() {}

    public static Single4 getInstance() {

        if (instance == null) {

            synchronized (Single4.class) {

                if (instance == null) {

                    instance = new Single4();

                }

            }

        }

        return instance;

    }

}


volatile關(guān)鍵字的一個(gè)作用是禁止指令重排,把instance聲明為volatile之后,對(duì)它的寫操作就會(huì)有一個(gè)內(nèi)存屏障(什么是內(nèi)存屏障?),這樣,在它的賦值完成之前,就不用會(huì)調(diào)用讀操作。


注意:volatile阻止的不singleton = new Singleton()這句話內(nèi)部[1-2-3]的指令重排,而是保證了在一個(gè)寫操作([1-2-3])完成之前,不會(huì)調(diào)用讀操作(if (instance == null))。


——也就徹底防止了Version3中的問(wèn)題發(fā)生。

——好了,現(xiàn)在徹底沒什么問(wèn)題了吧?

……

……

……


好了,別緊張,的確沒問(wèn)題了。大名鼎鼎的EventBus中,其入口方法EventBus.getDefault()就是用這種方法來(lái)實(shí)現(xiàn)的。

……

……

……

不過(guò),非要挑點(diǎn)刺的話還是能挑出來(lái)的,就是這個(gè)寫法有些復(fù)雜了,不夠優(yōu)雅、簡(jiǎn)潔。


4. 餓漢式單例


下面再聊了解一下餓漢式的單例。


如上所說(shuō),餓漢式單例是指:指全局的單例實(shí)例在類裝載時(shí)構(gòu)建的實(shí)現(xiàn)方式。


由于類裝載的過(guò)程是由類加載器(ClassLoader)來(lái)執(zhí)行的,這個(gè)過(guò)程也是由JVM來(lái)保證同步的,所以這種方式先天就有一個(gè)優(yōu)勢(shì)——能夠免疫許多由多線程引起的問(wèn)題。


4.1 餓漢式單例的實(shí)現(xiàn)方式


餓漢式單例的實(shí)現(xiàn)如下:


//餓漢式實(shí)現(xiàn)

public class SingleB {

    private static final SingleB INSTANCE = new SingleB();

    private SingleB() {}

    public static SingleB getInstance() {

        return INSTANCE;

    }

}


對(duì)于一個(gè)餓漢式單例的寫法來(lái)說(shuō),它基本上是完美的了。


所以它的缺點(diǎn)也就只是餓漢式單例本身的缺點(diǎn)所在了——由于INSTANCE的初始化是在類加載時(shí)進(jìn)行的,而類的加載是由ClassLoader來(lái)做的,所以開發(fā)者本來(lái)對(duì)于它初始化的時(shí)機(jī)就很難去準(zhǔn)確把握:


  1. 可能由于初始化的太早,造成資源的浪費(fèi)


  2. 如果初始化本身依賴于一些其他數(shù)據(jù),那么也就很難保證其他數(shù)據(jù)會(huì)在它初始化之前準(zhǔn)備好。


當(dāng)然,如果所需的單例占用的資源很少,并且也不依賴于其他數(shù)據(jù),那么這種實(shí)現(xiàn)方式也是很好的。


知識(shí)點(diǎn):什么時(shí)候是類裝載時(shí)?


前面提到了單例在類裝載時(shí)被實(shí)例化,那究竟什么時(shí)候才是『類裝載時(shí)』呢?


不嚴(yán)格的說(shuō),大致有這么幾個(gè)條件會(huì)觸發(fā)一個(gè)類被加載:


1. new一個(gè)對(duì)象時(shí)

2. 使用反射創(chuàng)建它的實(shí)例時(shí)

3. 子類被加載時(shí),如果父類還沒被加載,就先加載父類

4. jvm啟動(dòng)時(shí)執(zhí)行的主類會(huì)首先被加載


5. 一些其他的實(shí)現(xiàn)方式


5.1 Effective Java 1 —— 靜態(tài)內(nèi)部類


《Effective Java》一書的第一版中推薦了一個(gè)中寫法:


// Effective Java 第一版推薦寫法

public class Singleton {

    private static class SingletonHolder {

        private static final Singleton INSTANCE = new Singleton();

    }

    private Singleton (){}

    public static final Singleton getInstance() {

        return SingletonHolder.INSTANCE;

    }

}


這種寫法非常巧妙:


  • 對(duì)于內(nèi)部類SingletonHolder,它是一個(gè)餓漢式的單例實(shí)現(xiàn),在SingletonHolder初始化的時(shí)候會(huì)由ClassLoader來(lái)保證同步,使INSTANCE是一個(gè)真·單例。


  • 同時(shí),由于SingletonHolder是一個(gè)內(nèi)部類,只在外部類的Singleton的getInstance()中被使用,所以它被加載的時(shí)機(jī)也就是在getInstance()方法第一次被調(diào)用的時(shí)候。


——它利用了ClassLoader來(lái)保證了同步,同時(shí)又能讓開發(fā)者控制類加載的時(shí)機(jī)。從內(nèi)部看是一個(gè)餓漢式的單例,但是從外部看來(lái),又的確是懶漢式的實(shí)現(xiàn)。


簡(jiǎn)直是神乎其技。


5.2 Effective Java 2 —— 枚舉


你以為到這就算完了?不,并沒有,因?yàn)閰柡Φ拇笊裼职l(fā)現(xiàn)了其他的方法。


《Effective Java》的作者在這本書的第二版又推薦了另外一種方法,來(lái)直接看代碼:


// Effective Java 第二版推薦寫法

public enum SingleInstance {

    INSTANCE;

    public void fun1() { 

        // do something

    }

}

 

// 使用

SingleInstance.INSTANCE.fun1();


看到了么?這是一個(gè)枚舉類型……連class都不用了,極簡(jiǎn)。


由于創(chuàng)建枚舉實(shí)例的過(guò)程是線程安全的,所以這種寫法也沒有同步的問(wèn)題。


作者對(duì)這個(gè)方法的評(píng)價(jià):


這種寫法在功能上與共有域方法相近,但是它更簡(jiǎn)潔,無(wú)償?shù)靥峁┝诵蛄谢瘷C(jī)制,絕對(duì)防止對(duì)此實(shí)例化,即使是在面對(duì)復(fù)雜的序列化或者反射攻擊的時(shí)候。雖然這中方法還沒有廣泛采用,但是單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法。


枚舉單例這種方法問(wèn)世一些,許多分析文章都稱它是實(shí)現(xiàn)單例的最完美方法——寫法超級(jí)簡(jiǎn)單,而且又能解決大部分的問(wèn)題。


不過(guò)我個(gè)人認(rèn)為這種方法雖然很優(yōu)秀,但是它仍然不是完美的——比如,在需要繼承的場(chǎng)景,它就不適用了。


6. 總結(jié)


OK,看到這里,你還會(huì)覺得單例模式是最簡(jiǎn)單的設(shè)計(jì)模式了么?再回頭看一下你之前代碼中的單例實(shí)現(xiàn),覺得是無(wú)懈可擊的么?


可能我們?cè)趯?shí)際的開發(fā)中,對(duì)單例的實(shí)現(xiàn)并沒有那么嚴(yán)格的要求。比如,我如果能保證所有的getInstance都是在一個(gè)線程的話,那其實(shí)第一種最簡(jiǎn)單的教科書方式就夠用了。再比如,有時(shí)候,我的單例變成了多例也可能對(duì)程序沒什么太大影響……


但是,如果我們能了解更多其中的細(xì)節(jié),那么如果哪天程序出了些問(wèn)題,我們起碼能多一個(gè)排查問(wèn)題的點(diǎn)。早點(diǎn)解決問(wèn)題,就能早點(diǎn)回家吃飯……


—— 還有,完美的方案是不存在,任何方式都會(huì)有一個(gè)『度』的問(wèn)題。比如,你的覺得代碼已經(jīng)無(wú)懈可擊了,但是因?yàn)槟阌玫氖荍AVA語(yǔ)言,可能ClassLoader有些BUG啊……你的代碼誰(shuí)運(yùn)行在JVM上的,可能JVM本身有BUG啊……你的代碼運(yùn)行在手機(jī)上,可能手機(jī)系統(tǒng)有問(wèn)題啊……你生活在這個(gè)宇宙里,可能宇宙本身有些BUG啊……


所以,盡力做到能做到的最好就行了。


—— 感謝你花費(fèi)了不少時(shí)間看到這里,但愿你沒有覺得虛度。


7. 一些有用的鏈接


  • 深入淺出單實(shí)例SINGLETON設(shè)計(jì)模式:http:///articles/265.html


  • Java并發(fā)編程:volatile關(guān)鍵字解析:http://www.cnblogs.com/dolphin0520/p/3920373.html


  • 為什么volatile不能保證原子性而Atomic可以?: http://www.cnblogs.com/Mainz/p/3556430.html


  • 類在什么時(shí)候加載和初始化?http://www./6579.html


8. 關(guān)于作者


  • https://github.com/barryhappy

  • http://www.


看完本文有收獲?請(qǐng)轉(zhuǎn)發(fā)分享給更多人

關(guān)注「ImportNew」,提升Java技能

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    久久国产精品热爱视频| 丰满少妇被猛烈撞击在线视频| 国产无摭挡又爽又色又刺激| 九九热这里只有精品哦| 成人午夜免费观看视频| 深夜视频成人在线观看| 国产亚洲欧美一区二区| 一区中文字幕人妻少妇| 黄色片国产一区二区三区| 色狠狠一区二区三区香蕉蜜桃| 国产美女网红精品演绎| 精品一区二区三区不卡少妇av | 日本在线高清精品人妻| 亚洲成人免费天堂诱惑| 麻豆视频传媒入口在线看| 亚洲国产另类久久精品| 久久精品蜜桃一区二区av| 国产av精品高清一区二区三区| 操白丝女孩在线观看免费高清| 一区二区三区四区亚洲另类| 麻豆一区二区三区精品视频| 男人和女人干逼的视频| 欧美区一区二在线播放| 中文文精品字幕一区二区| 高清一区二区三区四区五区| 爽到高潮嗷嗷叫之在现观看| 亚洲熟女国产熟女二区三区| 日韩女优精品一区二区三区| 久久精品国产在热久久| 国产在线日韩精品欧美| 高清不卡视频在线观看| 欧洲一级片一区二区三区| 日本淫片一区二区三区| 好吊日视频这里都是精品| 日韩一区二区三区久久| 欧美不卡高清一区二区三区| 激情五月天免费在线观看| 不卡在线播放一区二区三区| 五月婷婷综合缴情六月| 成年午夜在线免费视频| 日韩女优精品一区二区三区|