鍍金池/ 教程/ Java/ 確保對象的唯一性——單例模式 (三)
工廠三兄弟之抽象工廠模式(五)
復(fù)雜對象的組裝與創(chuàng)建——建造者模式(一)
工廠三兄弟之工廠方法模式(一)
復(fù)雜對象的組裝與創(chuàng)建——建造者模式(二)
確保對象的唯一性——單例模式 (二)
工廠三兄弟之簡單工廠模式(四)
確保對象的唯一性——單例模式 (一)
工廠三兄弟之工廠方法模式(四)
對象的克隆——原型模式(一)
工廠三兄弟之抽象工廠模式(二)
工廠三兄弟之工廠方法模式(三)
工廠三兄弟之抽象工廠模式(一)
工廠三兄弟之抽象工廠模式(四)
確保對象的唯一性——單例模式 (三)
工廠三兄弟之簡單工廠模式(三)
對象的克隆——原型模式(二)
復(fù)雜對象的組裝與創(chuàng)建——建造者模式(三)
對象的克隆——原型模式(四)
確保對象的唯一性——單例模式(四)
工廠三兄弟之簡單工廠模式(一)
工廠三兄弟之簡單工廠模式(二)
對象的克隆——原型模式(三)
工廠三兄弟之抽象工廠模式(三)
確保對象的唯一性——單例模式(五)
工廠三兄弟之工廠方法模式(二)

確保對象的唯一性——單例模式 (三)

餓漢式單例與懶漢式單例的討論

Sunny 公司開發(fā)人員使用單例模式實(shí)現(xiàn)了負(fù)載均衡器的設(shè)計(jì),但是在實(shí)際使用中出現(xiàn)了一個(gè)非常嚴(yán)重的問題,當(dāng)負(fù)載均衡器在啟動(dòng)過程中用戶再次啟動(dòng)該負(fù)載均衡器時(shí),系統(tǒng)無任何異常,但當(dāng)客戶端提交請求時(shí)出現(xiàn)請求分發(fā)失敗,通過仔細(xì)分析發(fā)現(xiàn)原來系統(tǒng)中還是存在多個(gè)負(fù)載均衡器對象,導(dǎo)致分發(fā)時(shí)目標(biāo)服務(wù)器不一致,從而產(chǎn)生沖突。為什么會這樣呢?Sunny 公司開發(fā)人員百思不得其解。

現(xiàn)在我們對負(fù)載均衡器的實(shí)現(xiàn)代碼進(jìn)行再次分析,當(dāng)?shù)谝淮握{(diào)用 getLoadBalancer() 方法創(chuàng)建并啟動(dòng)負(fù)載均衡器時(shí),instance 對象為 null 值,因此系統(tǒng)將執(zhí)行代碼 instance= new LoadBalancer(),在此過程中,由于要對 LoadBalancer 進(jìn)行大量初始化工作,需要一段時(shí)間來創(chuàng)建 LoadBalancer 對象。而在此時(shí),如果再一次調(diào)用 getLoadBalancer() 方法(通常發(fā)生在多線程環(huán)境中),由于instance尚未創(chuàng)建成功,仍為 null 值,判斷條件(instance== null)為真值,因此代碼 instance= new LoadBalancer() 將再次執(zhí)行,導(dǎo)致最終創(chuàng)建了多個(gè) instance 對象,這違背了單例模式的初衷,也導(dǎo)致系統(tǒng)運(yùn)行發(fā)生錯(cuò)誤。

如何解決該問題?我們至少有兩種解決方案,在正式介紹這兩種解決方案之前,先介紹一下單例類的兩種不同實(shí)現(xiàn)方式,餓漢式單例類和懶漢式單例類。

餓漢式單例類

餓漢式單例類是實(shí)現(xiàn)起來最簡單的單例類,餓漢式單例類結(jié)構(gòu)圖如圖所示:

http://wiki.jikexueyuan.com/project/design-pattern-creation/images/1333305889_1823.gif" alt="" />

從圖中可以看出,由于在定義靜態(tài)變量的時(shí)候?qū)嵗瘑卫?,因此在類加載的時(shí)候就已經(jīng)創(chuàng)建了單例對象,代碼如下所示:

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

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

當(dāng)類被加載時(shí),靜態(tài)變量 instance 會被初始化,此時(shí)類的私有構(gòu)造函數(shù)會被調(diào)用,單例類的唯一實(shí)例將被創(chuàng)建。如果使用餓漢式單例來實(shí)現(xiàn)負(fù)載均衡器 LoadBalancer 類的設(shè)計(jì),則不會出現(xiàn)創(chuàng)建多個(gè)單例對象的情況,可確保單例對象的唯一性。

懶漢式單例類與線程鎖定

除了餓漢式單例,還有一種經(jīng)典的懶漢式單例,也就是前面的負(fù)載均衡器 LoadBalancer 類的實(shí)現(xiàn)方式。懶漢式單例類結(jié)構(gòu)圖如圖所示:

http://wiki.jikexueyuan.com/project/design-pattern-creation/images/1333305983_8045.gif" alt="" />

從圖中可以看出,懶漢式單例在第一次調(diào)用 getInstance() 方法時(shí)實(shí)例化,在類加載時(shí)并不自行實(shí)例化,這種技術(shù)又稱為延遲加載(Lazy Load)技術(shù),即需要的時(shí)候再加載實(shí)例,為了避免多個(gè)線程同時(shí)調(diào)用 getInstance() 方法,我們可以使用關(guān)鍵字 synchronized,代碼如下所示:

class LazySingleton {   
    private static LazySingleton instance = null;   

    private LazySingleton() { }   

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

該懶漢式單例類在 getInstance() 方法前面增加了關(guān)鍵字 synchronized 進(jìn)行線程鎖,以處理多個(gè)線程同時(shí)訪問的問題。但是,上述代碼雖然解決了線程安全問題,但是每次調(diào)用 getInstance() 時(shí)都需要進(jìn)行線程鎖定判斷,在多線程高并發(fā)訪問環(huán)境中,將會導(dǎo)致系統(tǒng)性能大大降低。如何既解決線程安全問題又不影響系統(tǒng)性能呢?我們繼續(xù)對懶漢式單例進(jìn)行改進(jìn)。事實(shí)上,我們無須對整個(gè) getInstance() 方法進(jìn)行鎖定,只需對其中的代碼“instance = new LazySingleton();”進(jìn)行鎖定即可。因此 getInstance() 方法可以進(jìn)行如下改進(jìn):

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

問題貌似得以解決,事實(shí)并非如此。如果使用以上代碼來實(shí)現(xiàn)單例,還是會存在單例對象不唯一。原因如下:

假如在某一瞬間線程A和線程B都在調(diào)用 getInstance() 方法,此時(shí) instance 對象為 null 值,均能通過 instance == null 的判斷。由于實(shí)現(xiàn)了 synchronized 加鎖機(jī)制,線程 A 進(jìn)入 synchronized 鎖定的代碼中執(zhí)行實(shí)例創(chuàng)建代碼,線程 B 處于排隊(duì)等待狀態(tài),必須等待線程 A 執(zhí)行完畢后才可以進(jìn)入 synchronized 鎖定代碼。但當(dāng) A 執(zhí)行完畢時(shí),線程B并不知道實(shí)例已經(jīng)創(chuàng)建,將繼續(xù)創(chuàng)建新的實(shí)例,導(dǎo)致產(chǎn)生多個(gè)單例對象,違背單例模式的設(shè)計(jì)思想,因此需要進(jìn)行進(jìn)一步改進(jìn),在 synchronized 中再進(jìn)行一次(instance == null)判斷,這種方式稱為雙重檢查鎖定(Double-Check Locking)。使用雙重檢查鎖定實(shí)現(xiàn)的懶漢式單例類完整代碼如下所示:

class LazySingleton { 
    private volatile static LazySingleton instance = null; 

    private LazySingleton() { } 

    public static LazySingleton getInstance() { 
        //第一重判斷
        if (instance == null) {
            //鎖定代碼塊
            synchronized (LazySingleton.class) {
                //第二重判斷
                if (instance == null) {
                    instance = new LazySingleton(); //創(chuàng)建單例實(shí)例
                }
            }
        }
        return instance; 
    }
}

需要注意的是,如果使用雙重檢查鎖定來實(shí)現(xiàn)懶漢式單例類,需要在靜態(tài)成員變量 instance 之前增加修飾符 volatile,被 volatile 修飾的成員變量可以確保多個(gè)線程都能夠正確處理,且該代碼只能在 JDK 1.5 及以上版本中才能正確執(zhí)行。由于 volatile 關(guān)鍵字會屏蔽 Java 虛擬機(jī)所做的一些代碼優(yōu)化,可能會導(dǎo)致系統(tǒng)運(yùn)行效率降低,因此即使使用雙重檢查鎖定來實(shí)現(xiàn)單例模式也不是一種完美的實(shí)現(xiàn)方式。

擴(kuò)展

IBM 公司高級軟件工程師 Peter Haggar 2004 年在 IBM developerWorks 上發(fā)表了一篇名為《雙重檢查鎖定及單例模式——全面理解這一失效的編程習(xí)語》的文章,對JDK 1.5 之前的雙重檢查鎖定及單例模式進(jìn)行了全面分析和闡述。

餓漢式單例類與懶漢式單例類比較

餓漢式單例類在類被加載時(shí)就將自己實(shí)例化,它的優(yōu)點(diǎn)在于無須考慮多線程訪問問題,可以確保實(shí)例的唯一性;從調(diào)用速度和反應(yīng)時(shí)間角度來講,由于單例對象一開始就得以創(chuàng)建,因此要優(yōu)于懶漢式單例。但是無論系統(tǒng)在運(yùn)行時(shí)是否需要使用該單例對象,由于在類加載時(shí)該對象就需要?jiǎng)?chuàng)建,因此從資源利用效率角度來講,餓漢式單例不及懶漢式單例,而且在系統(tǒng)加載時(shí)由于需要?jiǎng)?chuàng)建餓漢式單例對象,加載時(shí)間可能會比較長。

懶漢式單例類在第一次使用時(shí)創(chuàng)建,無須一直占用系統(tǒng)資源,實(shí)現(xiàn)了延遲加載,但是必須處理好多個(gè)線程同時(shí)訪問的問題,特別是當(dāng)單例類作為資源控制器,在實(shí)例化時(shí)必然涉及資源初始化,而資源初始化很有可能耗費(fèi)大量時(shí)間,這意味著出現(xiàn)多線程同時(shí)首次引用此類的機(jī)率變得較大,需要通過雙重檢查鎖定等機(jī)制進(jìn)行控制,這將導(dǎo)致系統(tǒng)性能受到一定影響。