天天看點

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

繼續閱讀