Java設計模式之《單例模式》及應用場景


所謂單例,指的就是單實例,有且僅有一個類實例,這個單例不應該由人來控制,而應該由代碼來限制,強制單例。

  單例有其獨有的使用場景,一般是對於那些業務邏輯上限定不能多例只能單例的情況,例如:類似於計數器之類的存在,一般都需要使用一個實例來進行記錄,若多例計數則會不准確。

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

  所以我認為有關單例模式的重點不在於場景,而在於如何使用。

1、常見的單例模式有兩種創建方式:所謂懶漢式與餓漢式

(1)懶漢式

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

詳見下方代碼示例:

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

 

(2)餓漢式

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

詳見下方代碼示例:

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

2、雙重加鎖機制

  何為雙重加鎖機制?

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

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進行加鎖的嗎?

  ......

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

  補充:關於鎖內部的第二重空判斷的作用,當多個線程一起到達鎖位置時,進行鎖競爭,其中一個線程獲取鎖,如果是第一次進入則dl為null,會進行單例對象的創建,完成后釋放鎖,其他線程獲取鎖后就會被空判斷攔截,直接返回已創建的單例對象。

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

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

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

3、類級內部類方式

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

  貌似有的,就是使用類級內部類結合多線程默認同步鎖,同時實現延遲加載和線程安全。

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

  

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

  其實使用靜態初始化器的方式會在類加載時創建類的實例,但是我們將實例的創建顯式放置在靜態內部類中,它會導致在外部類加載時不進行實例創建,這樣就能實現我們的雙重目的:延遲加載和線程安全。

4、使用

  在Spring中創建的Bean實例默認都是單例模式存在的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM