設(shè)計(jì)模式02_單例模式_Singleton

1.什么情況下使用單例模式?

有些對象只有一個,比如配置文件,工具類,線程池,緩存,日志對象等等。單例模式保證應(yīng)用中有且只有一個實(shí)例。

2. 什么是單例?

2.1、單例定義

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

2.2、單例的實(shí)現(xiàn)可以分為兩大類

懶漢式:指全局的單例實(shí)例在第一次被使用時構(gòu)建。
餓漢式:指全局的單例實(shí)例在類裝載時構(gòu)建。
注:日常我們使用的較多的應(yīng)該是懶漢式的單例,畢竟按需加載才能做到資源的最大化利用。

3. 懶漢式單例

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

3.1 簡單版本

看最簡單的寫法Version 1:

public class LazySingleton {
    //1. Simplest version
    private static LazySingleton instance;
    private LazySingleton(){}
    public static LazySingleton getInstance(){
        if(instance == null){
            instance =  new LazySingleton();
        }
        return instance;
    }
}

把構(gòu)造器改為私有的,這樣能夠防止被外部的類調(diào)用。每次獲取instance之前先進(jìn)行判斷,如果instance為空就new一個出來,否則就直接返回已存在的instance。這種寫法在大多數(shù)的時候也是沒問題的。問題在于,當(dāng)多線程工作的時候,如果有多個線程同時運(yùn)行到if (instance == null),都判斷為null,那么兩個線程就各自會創(chuàng)建一個實(shí)例——這樣一來,就不是單例了。

3.2 synchronized版本

那既然可能會因?yàn)槎嗑€程導(dǎo)致問題,那么加上一個同步鎖吧!修改后的代碼如下,相對于Version1,只是在方法簽名上多加了一個synchronized:

//2. Sychronized version
    private static LazySingleton instance2;
    private LazySingleton(){}
    public static synchronized LazySingleton getInstance2(){
        if(instance2 == null){
            instance2 = new LazySingleton();
        }
        return instance2;
    }

OK,加上synchronized關(guān)鍵字之后,getInstance方法就會鎖上了。如果有兩個線程(T1、T2)同時執(zhí)行到這個方法時,會有其中一個線程T1獲得同步鎖,得以繼續(xù)執(zhí)行,而另一個線程T2則需要等待,當(dāng)?shù)赥1執(zhí)行完畢getInstance之后(完成了null判斷、對象創(chuàng)建、獲得返回值之后),T2線程才會執(zhí)行執(zhí)行?!赃@端代碼也就避免了Version1中,可能出現(xiàn)因?yàn)槎嗑€程導(dǎo)致多個實(shí)例的情況。但是,這種寫法也有一個問題:給getInstance方法加鎖,雖然會避免了可能會出現(xiàn)的多個實(shí)例問題,但是會強(qiáng)制除T1之外的所有線程等待,實(shí)際上會對程序的執(zhí)行效率造成負(fù)面影響。

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

Version2代碼相對于Version1d代碼的效率問題,其實(shí)是為了解決1%幾率的問題,而使用了一個100%出現(xiàn)的防護(hù)盾。那有一個優(yōu)化的思路,就是把100%出現(xiàn)的防護(hù)盾,也改為1%的幾率出現(xiàn),使之只出現(xiàn)在可能會導(dǎo)致多個實(shí)例出現(xiàn)的地方。——有沒有這樣的方法呢?當(dāng)然是有的,改進(jìn)后的代碼Vsersion3如下:

    //3. Double-check version
    private static LazySingleton instance3;
    private LazySingleton(){}
    public static LazySingleton getInstance3(){
        if(instance3 == null){
            synchronized (LazySingleton.class){
                if(instance3 == null){
                    instance3 = new LazySingleton();
                }
            }
        }
        return instance3;
    }

第一個if (instance == null),其實(shí)是為了解決Version2中的效率問題,只有instance為null的時候,才進(jìn)入synchronized的代碼段大大減少了幾率。
第二個if (instance == null),則是跟Version2一樣,是為了防止可能出現(xiàn)多個實(shí)例的情況。
這段代碼看起來已經(jīng)完美無瑕了?!?當(dāng)然,只是『看起來』,還是有小概率出現(xiàn)問題的。這弄清楚為什么這里可能出現(xiàn)問題,首先,我們需要弄清楚幾個概念:原子操作、指令重排。
主要在于singleton = new Singleton()這句,這并非是一個原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情。
  1. 給 singleton 分配內(nèi)存
  2. 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量,形成實(shí)例
  3. 將singleton對象指向分配的內(nèi)存空間(執(zhí)行完這步 singleton才是非 null 了)但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化。
  也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報(bào)錯。
  再稍微解釋一下,就是說,由于有一個『instance已經(jīng)不為null但是仍沒有完成初始化』的中間狀態(tài),而這個時候,如果有其他線程剛好運(yùn)行到第一層if (instance == null)這里,這里讀取到的instance已經(jīng)不為null了,所以就直接把這個中間狀態(tài)的instance拿去用了,就會產(chǎn)生問題。這里的關(guān)鍵在于——線程T1對instance的寫操作沒有完成,線程T2就執(zhí)行了讀操作。

3.4 終極版本:volatile

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

//4. Double-check with volatile version
    private static volatile LazySingleton instance4;
    private LazySingleton(){}
    public static LazySingleton getInstance4(){
        if(instance4 == null){
            synchronized (LazySingleton.class){
                if(instance4 == null){
                    instance4 = new LazySingleton();
                }
            }
        }
        return instance4;
    }

一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:

1)可見性:保證了不同線程對這個變量進(jìn)行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

2)有序性:禁止進(jìn)行指令重排序。
我的理解是,volatile修飾后,保證了singleton = new Singleton()這句話的指令執(zhí)行順序,從而不會出現(xiàn)版本3的問題。

4. 餓漢式單例

下面再聊了解一下餓漢式的單例。
  如上所說,餓漢式單例是指:指全局的單例實(shí)例在類裝載時構(gòu)建的實(shí)現(xiàn)方式。
由于類裝載的過程是由類加載器(ClassLoader)來執(zhí)行的,這個過程也是由JVM來保證同步的,所以這種方式先天就有一個優(yōu)勢——能夠免疫許多由多線程引起的問題。

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

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

public class HungreySingleton {
    private static final HungreySingleton instance = new HungreySingleton();
    private HungreySingleton(){}
    public static HungreySingleton getInstance() {
        return instance;
    }
}

對于一個餓漢式單例的寫法來說,它基本上是完美的了。所以它的缺點(diǎn)也就只是餓漢式單例本身的缺點(diǎn)所在了——由于INSTANCE的初始化是在類加載時進(jìn)行的,而類的加載是由ClassLoader來做的,所以開發(fā)者本來對于它初始化的時機(jī)就很難去準(zhǔn)確把握:可能由于初始化的太早,造成資源的浪費(fèi)。
  如果初始化本身依賴于一些其他數(shù)據(jù),那么也就很難保證其他數(shù)據(jù)會在它初始化之前準(zhǔn)備好。
  當(dāng)然,如果所需的單例占用的資源很少,并且也不依賴于其他數(shù)據(jù),那么這種實(shí)現(xiàn)方式也是很好的。

4.2什么時候是類裝載時?

類從被加載到虛擬機(jī)內(nèi)存中開始,直到卸載出內(nèi)存為止,它的整個生命周期包括了:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載這7個階段。其中,驗(yàn)證、準(zhǔn)備和解析這三個部分統(tǒng)稱為連接(linking)。
什么情況下需要開始類加載過程的第一個階段:"加載"。虛擬機(jī)規(guī)范中并沒強(qiáng)行約束,這點(diǎn)可以交給虛擬機(jī)的的具體實(shí)現(xiàn)自由把握,但是對于初始化階段虛擬機(jī)規(guī)范是嚴(yán)格規(guī)定了如下幾種情況,如果類未初始化會對類進(jìn)行初始化。

  • 創(chuàng)建類的實(shí)例
  • 訪問類的靜態(tài)變量(除常量【被final修辭的靜態(tài)變量】原因:常量一種特殊的變量,因?yàn)榫幾g器把他們當(dāng)作值(value)而不是域(field)來對待。如果你的代碼中用到了常變量(constant variable),編譯器并不會生成字節(jié)碼來從對象中載入域的值,而是直接把這個值插入到字節(jié)碼中。這是一種很有用的優(yōu)化,但是如果你需要改變final域的值那么每一塊用到那個域的代碼都需要重新編譯。
  • 訪問類的靜態(tài)方法
  • 反射如(Class.forName("my.xyz.Test"))
  • 當(dāng)初始化一個類時,發(fā)現(xiàn)其父類還未初始化,則先出發(fā)父類的初始化
  • 虛擬機(jī)啟動時,定義了main()方法的那個類先初始化
class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;
 
    private SingleTon() {
        count1++;
        count2++;
    }
 
    public static SingleTon getInstance() {
        return singleTon;
    }
}
 
public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}
輸出:
count1=1
count2=0
分析:
1:SingleTon singleTon = SingleTon.getInstance();調(diào)用了類的SingleTon調(diào)用了類的靜態(tài)方法,觸發(fā)類的初始化
2:類加載的時候在準(zhǔn)備過程中為類的靜態(tài)變量分配內(nèi)存并初始化默認(rèn)值 singleton=null count1=0,count2=0
3:類初始化,為類的靜態(tài)變量賦值和執(zhí)行靜態(tài)代碼快。singleton賦值為new SingleTon()調(diào)用類的構(gòu)造方法
4:調(diào)用類的構(gòu)造方法后count=1;count2=1
5:繼續(xù)為count1與count2賦值,此時count1沒有賦值操作,所有count1為1,但是count2執(zhí)行賦值操作就變?yōu)?

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

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

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

public class InnerSingleton {
    private static class SingletonHolder{
        private static final InnerSingleton instance = new InnerSingleton();
    }
    private InnerSingleton(){}
    public static final InnerSingleton getInstance(){
        return SingletonHolder.instance;
    }
}

這種寫法非常巧妙:對于內(nèi)部類SingletonHolder,它是一個餓漢式的單例實(shí)現(xiàn),在SingletonHolder初始化的時候會由ClassLoader來保證同步,使INSTANCE是一個真·單例。
  同時,由于SingletonHolder是一個內(nèi)部類,只在外部類的Singleton的getInstance()中被使用,所以它被加載的時機(jī)也就是在getInstance()方法第一次被調(diào)用的時候。
  它利用了ClassLoader來保證了同步,同時又能讓開發(fā)者控制類加載的時機(jī)。從內(nèi)部看是一個餓漢式的單例,但是從外部看來,又的確是懶漢式的實(shí)現(xiàn)**。簡直是神乎其技。

5.2 Effective Java 2 —— 枚舉

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

public enum SingleInstance {
   INSTANCE;
    public void fun1() {
        // do something
    }
}// 使用SingleInstance.INSTANCE.fun1();

看到了么?這是一個枚舉類型……連class都不用了,極簡。由于創(chuàng)建枚舉實(shí)例的過程是線程安全的,所以這種寫法也沒有同步的問題。

對這個方法的評價(jià):
  這種寫法在功能上與共有域方法相近,但是它更簡潔,無償?shù)靥峁┝诵蛄谢瘷C(jī)制,絕對防止對此實(shí)例化,即使是在面對復(fù)雜的序列化或者反射攻擊的時候。雖然這中方法還沒有廣泛采用,但是單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法。
  枚舉單例這種方法問世一些,許多分析文章都稱它是實(shí)現(xiàn)單例的最完美方法——寫法超級簡單,而且又能解決大部分的問題。不過我個人認(rèn)為這種方法雖然很優(yōu)秀,但是它仍然不是完美的——比如,在需要繼承的場景,它就不適用了。

6. 總結(jié)

OK,看到這里,你還會覺得單例模式是最簡單的設(shè)計(jì)模式了么?再回頭看一下你之前代碼中的單例實(shí)現(xiàn),覺得是無懈可擊的么?可能我們在實(shí)際的開發(fā)中,對單例的實(shí)現(xiàn)并沒有那么嚴(yán)格的要求。比如,我如果能保證所有的getInstance都是在一個線程的話,那其實(shí)第一種最簡單的教科書方式就夠用了。再比如,有時候,我的單例變成了多例也可能對程序沒什么太大影響……但是,如果我們能了解更多其中的細(xì)節(jié),那么如果哪天程序出了些問題,我們起碼能多一個排查問題的點(diǎn)。早點(diǎn)解決問題,就能早點(diǎn)回家吃飯……:-D
   還有,完美的方案是不存在,任何方式都會有一個『度』的問題。比如,你的覺得代碼已經(jīng)無懈可擊了,但是因?yàn)槟阌玫氖荍AVA語言,可能ClassLoader有些BUG啊……你的代碼誰運(yùn)行在JVM上的,可能JVM本身有BUG啊……你的代碼運(yùn)行在手機(jī)上,可能手機(jī)系統(tǒng)有問題啊……你生活在這個宇宙里,可能宇宙本身有些BUG啊……o(╯□╰)o所以,盡力做到能做到的最好就行了。
   感謝你花費(fèi)了不少時間看到這里,但愿你沒有覺得虛度。

本文僅對于原文作少許修改。
原文:
作者:博麟K
鏈接:http://www.itdecent.cn/p/d2755af464d2

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容