Java設(shè)計模式系列-單例模式


原創(chuàng)文章,轉(zhuǎn)載請標(biāo)注出處:《Java設(shè)計模式系列-單例模式》


一、概述

????????所謂單例,指的就是單實例,有且僅有一個類實例,這個單例不應(yīng)該由人來控制,而應(yīng)該由代碼來限制,強(qiáng)制單例。

????????單例有其獨(dú)有的使用場景,一般是對于那些業(yè)務(wù)邏輯上限定不能多例只能單例的情況,例如:類似于計數(shù)器之類的存在,一般都需要使用一個實例來進(jìn)行記錄,若多例計數(shù)則會不準(zhǔn)確。

????????其實單例就是那些很明顯的使用場合,沒有之前學(xué)習(xí)的那些模式所使用的復(fù)雜場景,只要你需要使用單例,那你就使用單例,簡單易理解。

????????所以我認(rèn)為有關(guān)單例模式的重點(diǎn)不在于場景,而在于如何使用。

二、單例實現(xiàn)

2.1 懶漢式

????????何為懶?顧名思義,就是不做事,這里也是同義,懶漢式就是不在系統(tǒng)加載時就創(chuàng)建類的單例,而是在第一次使用實例的時候再創(chuàng)建。

????????詳見下方代碼示例:

public class LHanDanli {
    // 定義一個私有類變量來存放單例,私有的目的是指外部無法
    // 直接獲取這個變量,而要使用提供的公共方法來獲取
    private static LHanDanli dl = null;
    // 定義私有構(gòu)造器,表示只在類內(nèi)部使用,
    // 亦指單例的實例只能在單例類內(nèi)部創(chuàng)建
    private LHanDanli(){}
    // 定義一個公共的公開的方法來返回該類的實例,
    // 由于是懶漢式,需要在第一次使用時生成實例,
    // 所以為了線程安全,使用synchronized關(guān)鍵字來確保只會生成單例
    public static synchronized LHanDanli getInstance(){
        if(dl == null){
            dl = new LHanDanli();
        }
        return dl;
    }
}

2.2 餓漢式

????????又何為餓?餓者,饑不擇食;但凡有食,必急食之。此處同義:在加載類的時候就會創(chuàng)建類的單例,并保存在類中。

????????詳見下方代碼示例:

public class EHanDanli {
    // 此處定義類變量實例并直接實例化,
    // 在類加載的時候就完成了實例化并保存在類中
    private static EHanDanli dl = new EHanDanli();
    // 定義無參構(gòu)造器,用于單例實例
    private EHanDanli(){}
    // 定義公開方法,返回已創(chuàng)建的單例
    public static EHanDanli getInstance(){
        return dl;
    }
}

2.3 雙重加鎖機(jī)制

????????何為雙重加鎖機(jī)制?

????????在懶漢式實現(xiàn)單例模式的代碼中,有使用synchronized關(guān)鍵字來同步獲取實例,保證單例的唯一性,但是上面的代碼在每一次執(zhí)行時都要進(jìn)行同步和判斷,無疑會拖慢速度,使用雙重加鎖機(jī)制正好可以解決這個問題:

public class SLHanDanli {
    private static volatile SLHanDanli dl = null;
    private SLHanDanli(){}
    public static SLHanDanli getInstance(){
        if(dl == null){
            synchronized (SLHanDanli.class) {
                if(dl == null){
                    dl = new SLHanDanli();
                }
            }
        }
        return dl;
    }
}

????????看了上面的代碼,有沒有感覺很無語,雙重加鎖難道不是需要兩個synchronized進(jìn)行加鎖的嗎?

????????...

????????其實不然,這里的雙重指的的雙重判斷,而加鎖單指那個synchronized,為什么要進(jìn)行雙重判斷,其實很簡單,第一重判斷,如果單例已經(jīng)存在,那么就不再需要進(jìn)行同步操作,而是直接返回這個實例,如果沒有創(chuàng)建,才會進(jìn)入同步塊,同步塊的目的與之前相同,目的是為了防止有兩個調(diào)用同時進(jìn)行時,導(dǎo)致生成多個實例,有了同步塊,每次只能有一個線程調(diào)用能訪問同步塊內(nèi)容,當(dāng)?shù)谝粋€搶到鎖的調(diào)用獲取了實例之后,這個實例就會被創(chuàng)建,之后的所有調(diào)用都不會進(jìn)入同步塊,直接在第一重判斷就返回了單例。至于第二個判斷,個人感覺有點(diǎn)查遺補(bǔ)漏的意味在內(nèi)(期待高人高見)。

????????補(bǔ)充:關(guān)于鎖內(nèi)部的第二重空判斷的作用,當(dāng)多個線程一起到達(dá)鎖位置時,進(jìn)行鎖競爭,其中一個線程獲取鎖,如果是第一次進(jìn)入則dl為null,會進(jìn)行單例對象的創(chuàng)建,完成后釋放鎖,其他線程獲取鎖后就會被空判斷攔截,直接返回已創(chuàng)建的單例對象。

????????不論如何,使用了雙重加鎖機(jī)制后,程序的執(zhí)行速度有了顯著提升,不必每次都同步加鎖。

????????其實我最在意的是volatile的使用,volatile關(guān)鍵字的含義是:被其所修飾的變量的值不會被本地線程緩存,所有對該變量的讀寫都是直接操作共享內(nèi)存來實現(xiàn),從而確保多個線程能正確的處理該變量。該關(guān)鍵字可能會屏蔽掉虛擬機(jī)中的一些代碼優(yōu)化,所以其運(yùn)行效率可能不是很高,所以,一般情況下,并不建議使用雙重加鎖機(jī)制,酌情使用才是正理!

????????更進(jìn)一步說,其實使用volatile的目的是為了防止暴露一個未初始化的不完整單例實例,導(dǎo)致系統(tǒng)崩潰。因為創(chuàng)建單例實例其實需要經(jīng)過以下幾步:首先分配內(nèi)存空間、然后將內(nèi)存空間的首地址指向引用(指針),最后調(diào)用構(gòu)造器創(chuàng)建實例,由于在第二步的時候這個引用(指針)就會變的非null,那么在第三步未執(zhí)行,真正的單例實例還未創(chuàng)建完成的時候,一個線程過來在第一個校驗中為false,將會直接將不完整的實例返回,從而造成系統(tǒng)崩潰。

2.4 類級內(nèi)部類方式

????????餓漢式會占用較多的空間,因為其在類加載時就會完成實例化,而懶漢式又存在執(zhí)行速率慢的情況,雙重加鎖機(jī)制呢?又有執(zhí)行效率差的毛病,有沒有一種完美的方式可以規(guī)避這些毛病呢?

????????貌似有的,就是使用類級內(nèi)部類結(jié)合多線程默認(rèn)同步鎖,同時實現(xiàn)延遲加載和線程安全。

public class ClassInnerClassDanli {
    public static class DanliHolder{
        private static ClassInnerClassDanli dl = new ClassInnerClassDanli();
    }
    private ClassInnerClassDanli(){}
    public static ClassInnerClassDanli getInstance(){
        return DanliHolder.dl;
    }
}

????????如上代碼,所謂類級內(nèi)部類,就是靜態(tài)內(nèi)部類,這種內(nèi)部類與其外部類之間并沒有從屬關(guān)系,加載外部類的時候,并不會同時加載其靜態(tài)內(nèi)部類,只有在發(fā)生調(diào)用的時候才會進(jìn)行加載,加載的時候就會創(chuàng)建單例實例并返回,有效實現(xiàn)了懶加載(延遲加載),至于同步問題,我們采用和餓漢式同樣的靜態(tài)初始化器的方式,借助JVM來實現(xiàn)線程安全。

????????其實使用靜態(tài)初始化器的方式會在類加載時創(chuàng)建類的實例,但是我們將實例的創(chuàng)建顯式放置在靜態(tài)內(nèi)部類中,它會導(dǎo)致在外部類加載時不進(jìn)行實例創(chuàng)建,這樣就能實現(xiàn)我們的雙重目的:延遲加載和線程安全。

四、使用

????????在Spring中創(chuàng)建的Bean實例默認(rèn)都是單例模式存在的。

最后編輯于
?著作權(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)容