Java——設計模式之單例模式詳解

一、單例模式定義

單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。

 

二、爲什麼要使用單例模式

1.對於系統中的某些類來說,只有一個實例很重要。例如,一個系統中可以存在多個打印任務,但是只能有一個正在工作的任務;售票時,一共有100張票,可有有多個窗口同時售票,但需要保證不要超售(這裏的票數餘量就是單例,售票涉及到多線程)。如果不是用機制對窗口對象進行唯一化將彈出多個窗口,如果這些窗口顯示的都是相同的內容,重複創建就會浪費資源。

2.有些類如果不控制成單例的結構,應用中就會存在很多一模一樣的類實例,這會非常浪費系統的內存資源,而且容易導致錯誤甚至一定會產生錯誤,所以我們單例模式所期待的目標或者說使用它的目的,是爲了儘可能的節約內存空間,減少無謂的GC消耗,並且使應用可以正常運作。

 

三、什麼時候使用單例模式

1.我們可以發現所有可以使用單例模式的類都有一個共性,那就是這個類沒有自己的狀態,換句話說,這些類無論你實例化多少個,其實都是一樣的。

2.在應用中如果有兩個或者兩個以上的實例會引起錯誤,又或者我換句話說,就是這些類,在整個應用中,同一時刻,有且只能有一種狀態。

應用場景:

需求:在前端創建工具箱窗口,工具箱要麼不出現,出現也只出現一個

遇到問題:每次點擊菜單都會重複創建“工具箱”窗口。

解決方案一:使用if語句,在每次創建對象的時候首先進行判斷是否爲null,如果爲null再創建對象。

需求:如果在5個地方需要實例出工具箱窗體

遇到問題:這個小bug需要改動5個地方,並且代碼重複,代碼利用率低

解決方案二:利用單例模式,保證一個類只有一個實例,並提供一個訪問它的全局訪問點。

 

四、 線程安全的問題

一方面,在使用單例對象的時候,要注意單例對象內的實例變量是會被多線程共享的,推薦使用無狀態的對象,不會因爲多個線程的交替調度而破壞自身狀態導致線程安全問題,比如我們常用的VO,DTO等(局部變量是在用戶棧中的,而且用戶棧本身就是線程私有的內存區域,所以不存在線程安全問題)。

另一方面在獲取單例的時候,要保證不能產生多個實例對象,下面詳細講到五種實現方式。

 

五、實現單例模式的方式

注:所有的單例模式都是使用靜態方法進行創建的,所以單例對象在內存中靜態共享區中存儲。

第一種(懶漢,線程不安全):

public class SingletonDemo1 {
    private static SingletonDemo1 instance;
    private SingletonDemo1(){}
    public static SingletonDemo1 getInstance(){
        if (instance == null) {
            instance = new SingletonDemo1();
        }
        return instance;
    }
}

該示例雖然用延遲加載方式實現了懶漢式單例,但在多線程環境下會產生多個single對象,如何改造請看以下方式:

第二種(懶漢,線程安全):

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

在方法上加synchronized同步鎖或是用同步代碼塊對類加同步鎖,此種方式雖然解決了多個實例對象問題,但是該方式運行效率卻很低下,下一個線程想要獲取對象,就必須等待上一個線程釋放鎖之後,纔可以繼續運行。

 第三種(餓漢):

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

這種方式基於classloder機制避免了多線程的同步問題,不過,instance在類裝載時就實例化,這時候初始化instance顯然沒有達到lazy loading的效果。

 第四種(漢,靜態代碼塊實現):

public class SingletonDemo4 {
    private static SingletonDemo4 instance = null;
    static{
        instance = new SingletonDemo4();
    }
    private SingletonDemo4(){}
    public static SingletonDemo4 getInstance(){
        return instance;
    }
}

表面上看起來差別挺大,其實更第三種方式差不多,都是在類初始化即實例化instance

第五種(枚舉):

public enum SingletonDemo6 {
    instance;
    public void whateverMethod(){
    }
}

 這種方式是Effective Java作者Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,可謂是很堅強的壁壘啊,不過,個人認爲由於1.5中才加入enum特性,用這種方式寫不免讓人感覺生疏,在實際工作中,我也很少看見有人這麼寫過。

第六種(雙重校驗鎖):

public class SynchronizedSingleton {

    //一個靜態的實例
    private static SynchronizedSingleton synchronizedSingleton;
    //私有化構造函數
    private SynchronizedSingleton(){}
    //給出一個公共的靜態方法返回一個單一實例
    public static SynchronizedSingleton getInstance(){
        if (synchronizedSingleton == null) {
            synchronized (SynchronizedSingleton.class) {
                if (synchronizedSingleton == null) {
                    synchronizedSingleton = new SynchronizedSingleton();
                }
            }
        }
        return synchronizedSingleton;
    }
}

這種做法與上面那種最無腦的同步做法相比就要好很多了,因爲我們只是在當前實例爲null,也就是實例還未創建時才進行同步,否則就直接返回,這樣就節省了很多無謂的線程等待時間,值得注意的是在同步塊中,我們再次判斷synchronizedSingleton是否爲null,解釋下爲什麼要這樣做。

假設我們去掉同步塊中的是否爲null的判斷,有這樣一種情況,假設A線程和B線程都在同步塊外面判斷了synchronizedSingleton爲null,結果A線程首先獲得了線程鎖,進入了同步塊,然後A線程會創造一個實例,此時synchronizedSingleton已經被賦予了實例,A線程退出同步塊,直接返回了第一個創造的實例,此時B線程獲得線程鎖,也進入同步塊,此時A線程其實已經創造好了實例,B線程正常情況應該直接返回的,但是因爲同步塊裏沒有判斷是否爲null,直接就是一條創建實例的語句,所以B線程也會創造一個實例返回,此時就造成創造了多個實例的情況。

經過剛纔的分析,貌似上述雙重加鎖的示例看起來是沒有問題了,但如果再進一步深入考慮的話,其實仍然是有問題的。

如果我們深入到JVM中去探索上面這段代碼,它就有可能(注意,只是有可能)是有問題的。

因爲虛擬機在執行創建實例的這一步操作的時候,其實是分了好幾步去進行的,也就是說創建一個新的對象並非是原子性操作。在有些JVM中上述做法是沒有問題的,但是有些情況下是會造成莫名的錯誤。

首先要明白在JVM創建新的對象時,主要要經過三步。

              1.分配內存

              2.初始化構造器

              3.將對象指向分配的內存的地址

這種順序在上述雙重加鎖的方式是沒有問題的,因爲這種情況下JVM是完成了整個對象的構造纔將內存的地址交給了對象。但是如果2和3步驟是相反的(2和3可能是相反的是因爲JVM會針對字節碼進行調優,而其中的一項調優便是調整指令的執行順序),就會出現問題了。

因爲這時將會先將內存地址賦給對象,針對上述的雙重加鎖,就是說先將分配好的內存地址指給synchronizedSingleton,然後再進行初始化構造器,這時候後面的線程去請求getInstance方法時,會認爲synchronizedSingleton對象已經實例化了,直接返回一個引用。如果在初始化構造器之前,這個線程使用了synchronizedSingleton,就會產生莫名的錯誤。

解決辦法:

1.給靜態的實例屬性加上關鍵字volatile,標識這個屬性是不需要優化的。

這樣也不會出現實例化發生一半的情況,因爲加入了volatile關鍵字,就等於禁止了JVM自動的指令重排序優化,並且強行保證線程中對變量所做的任何寫入操作對其他線程都是即時可見的。這裏沒有篇幅去介紹volatile以及JVM中變量訪問時所做的具體動作,總之volatile會強行將對該變量的所有讀和取操作綁定成一個不可拆分的動作。如果讀者有興趣的話,可以自行去找一些資料看一下相關內容。

不過值得注意的是,volatile關鍵字是在JDK1.5以及1.5之後才被給予了意義,所以這種方式要在JDK1.5以及1.5之後纔可以使用,但仍然還是不推薦這種方式,一是因爲代碼相對複雜,二是因爲由於JDK版本的限制有時候會有諸多不便。

2.將該任務交給JVM,所以有一種比較標準的單例模式。如下所示。

第七種(靜態內部類):

注:靜態內部類雖然保證了單例在多線程併發下的線程安全性,但是在遇到序列化對象時,默認的方式運行得到的結果就是多例的。這種情況不多做說明了,使用時請注意。

public class InnerClassSingleton {
    
    public static Singleton getInstance(){
        return Singleton.singleton;
    }

    private static class Singleton{
        
        protected static Singleton singleton = new Singleton();
        
    }
}

這種方式爲何會避免了上面莫名的錯誤,主要是因爲一個類的靜態屬性只會在第一次加載類時初始化,這是JVM幫我們保證的,所以我們無需擔心併發訪問的問題。所以在初始化進行一半的時候,別的線程是無法使用的,因爲JVM會幫我們強行同步這個過程。另外由於靜態變量只初始化一次,所以singleton仍然是單例的。 

 上述形式保證了以下幾點:

1.Singleton最多隻有一個實例,在不考慮反射強行突破訪問限制的情況下。

2.保證了併發訪問的情況下,不會發生由於併發而產生多個實例。

3.保證了併發訪問的情況下,不會由於初始化動作未完全完成而造成使用了尚未正確初始化的實例。

 

我們用另外一段代碼來說明一下靜態內部類實現單例:

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

這種方式同樣利用了classloder的機制來保證初始化instance時只有一個線程,它跟第三種和第四種方式不同的是(很細微的差別):第三種和第四種方式是隻要Singleton類被裝載了,那麼instance就會被實例化(沒有達到lazy loading效果),而這種方式是Singleton類被裝載了,instance不一定被初始化。因爲SingletonHolder類沒有被主動使用,只有顯示通過調用getInstance方法時,纔會顯示裝載SingletonHolder類,從而實例化instance。想象一下,如果實例化instance很消耗資源,我想讓他延遲加載,另外一方面,我不希望在Singleton類加載時就實例化,因爲我不能確保Singleton類還可能在其他的地方被主動使用從而被加載,那麼這個時候實例化instance顯然是不合適的。這個時候,這種方式相比第三和第四種方法就顯得更合理。

 

參考博客:

https://www.cnblogs.com/zuoxiaolong/p/pattern2.html

https://www.cnblogs.com/Ycheng/p/7169381.html

http://www.cnblogs.com/garryfu/p/7976546.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章