設計模式【1】-- 單例模式到底幾種寫法?

[TOC]java

單例模式,是一種比較簡單的設計模式,也是屬於建立型模式(提供一種建立對象的模式或者方式)。
要點:設計模式

    • 1.涉及一個單一的類,這個類來建立本身的對象(不能在其餘地方重寫建立方法,初始化類的時候建立或者提供私有的方法進行訪問或者建立,必須確保只有單個的對象被建立)。
    • 2.單例模式不必定是線程不安全的。
    • 3.單例模式能夠分爲兩種:懶漢模式(在第一次使用類的時候才建立,能夠理解爲類加載的時候特別懶,要用的時候纔去獲取,要是沒有就建立,因爲是單例,因此只有第一次使用的時候沒有,建立後就能夠一直用同一個對象),餓漢模式(在類加載的時候就已經建立,能夠理解爲餓漢已經餓得飢渴難耐,確定先把資源牢牢拽在本身手中,因此在類加載的時候就會先建立實例)安全

      關鍵字:多線程

      • 單例:singleton
      • 實例:instance
      • 同步: synchronized

    餓漢模式

    1.私有屬性

    第一種singlepublic,能夠直接經過Singleton類名來訪問。併發

    public class Singleton {
        // 私有化構造方法,以防止外界使用該構造方法建立新的實例
        private Singleton(){
        }
        // 默認是public,訪問能夠直接經過Singleton.instance來訪問
        static Singleton instance = new Singleton();
    }

    2.公有屬性

    第二種是用private修飾singleton,那麼就須要提供static 方法來訪問。函數

    public class Singleton {
        private Singleton(){
        }
        // 使用private修飾,那麼就須要提供get方法供外界訪問
        private static Singleton instance = new Singleton();
        // static將方法歸類全部,直接經過類名來訪問
        public static Singleton getInstance(){
            return instance;.
        }
    }

    3. 懶加載

    餓漢模式,這樣的寫法是沒有問題的,不會有線程安全問題(類的static成員建立的時候默認是上鎖的,不會同時被多個線程獲取到),可是是有缺點的,由於instance的初始化是在類加載的時候就在進行的,因此類加載是由ClassLoader來實現的,那麼初始化得比較早好處是後來直接能夠用,壞處也就是浪費了資源,要是隻是個別類使用這樣的方法,依賴的數據量比較少,那麼這樣的方法也是一種比較好的單例方法。
    在單例模式中通常是調用getInstance()方法來觸發類裝載,以上的兩種餓漢模式顯然沒有實現lazyload(我的理解是用的時候才觸發類加載)
    因此下面有一種餓漢模式的改進版,利用內部類實現懶加載。
    這種方式Singleton類被加載了,可是instance也不必定被初始化,要等到SingletonHolder被主動使用的時候,也就是顯式調用getInstance()方法的時候,纔會顯式的裝載SingletonHolder類,從而實例化instance。這種方法使用類裝載器保證了只有一個線程可以初始化instance,那麼也就保證了單例,而且實現了懶加載。學習

    值得注意的是:靜態內部類雖然保證了單例在多線程併發下的線程安全性,可是在遇到序列化對象時,默認的方式運行獲得的結果就是多例的。優化

    public class Singleton {
        private Singleton(){
        }
        //內部類
        private static class SingletonHolder{
            private static final Singleton instance = new Singleton();
        }
        //對外提供的不容許重寫的獲取方法
        public static final Singleton getInstance(){
            return SingletonHolder.instance;
        }
    }

    懶漢模式

    最基礎的代碼(線程不安全)線程

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

    這種寫法,是在每次獲取實例instance的時候進行判斷,若是沒有那麼就會new一個出來,不然就直接返回以前已經存在的instance。可是這樣的寫法不是線程安全的,當有多個線程都執行getInstance()方法的時候,都判斷是否等於null的時候,就會各自建立新的實例,這樣就不能保證單例了。因此咱們就會想到同步鎖,使用synchronized關鍵字:
    加同步鎖的代碼(線程安全,效率不高)設計

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

    這樣的話,getInstance()方法就會被鎖上,當有兩個線程同時訪問這個方法的時候,總會有一個線程先得到了同步鎖,那麼這個線程就能夠執行下去,而另外一個線程就必須等待,等待第一個線程執行完getInstance()方法以後,才能夠執行。這段代碼是線程安全的,可是效率不高,由於假若有不少線程,那麼就必須讓全部的都等待正在訪問的線程,這樣就會大大下降了效率。那麼咱們有一種思路就是,將鎖出現等待的機率再下降,也就是咱們所說的雙重校驗鎖(雙檢鎖)。

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

    1.第一個if判斷,是爲了下降鎖的出現機率,前一段代碼,只要執行到同一個方法都會觸發鎖,而這裏只有singleton爲空的時候纔會觸發,第一個進入的線程會建立對象,等其餘線程再進入時對象已建立就不會繼續建立,若是對整個方法同步,全部獲取單例的線程都要排隊,效率就會下降。
    2.第二個if判斷是和以前的代碼起同樣的做用。

    上面的代碼看起來已經像是沒有問題了,事實上,還有有很小的機率出現問題,那麼咱們先來了解:原子操做指令重排

    1.原子操做

    • 原子操做,能夠理解爲不可分割的操做,就是它已經小到不能夠再切分爲多個操做進行,那麼在計算機中要麼它徹底執行了,要麼它徹底沒有執行,它不會存在執行到中間狀態,能夠理解爲沒有中間狀態。好比:賦值語句就是一個原子操做:
    n = 1; //這是一個原子操做

    假設n的值之前是0,那麼這個操做的背後就是要麼執行成功n等於1,要麼沒有執行成功n等於0,不會存在中間狀態,就算是併發的過程當中也是同樣的。
    下面看一句不是原子操做的代碼:

    int n =1;  //不是原子操做

    緣由:這個語句中能夠拆分爲兩個操做,1.聲明變量n,2.給變量賦值爲1,從中咱們能夠看出有一種狀態是n被聲明後可是沒有來得及賦值的狀態,這樣的狀況,在併發中,若是多個線程同時使用n,那麼就會可能致使不穩定的結果。

    2.指令重排

    所謂指令重排,就是計算機會對咱們代碼進行優化,優化的過程當中會在不影響最後結果的前提下,調整原子操做的順序。好比下面的代碼:

    int a ;   // 語句1 
    a = 1 ;   // 語句2
    int b = 2 ;     // 語句3
    int c = a + b ; // 語句4

    正常的狀況,執行順序應該是1234,可是實際有多是3124,或者1324,這是由於語句3和4都沒有原子性問題,那麼就有可能被拆分紅原子操做,而後重排.
    原子操做以及指令重排的基本瞭解到這裏結束,看回咱們的代碼:

    主要是 instance = new Singleton(),根據咱們所說的,這個語句不是原子操做,那麼就會被拆分,事實上JVM(java虛擬機)對這個語句作的操做:
    • 1.給instance分配了內存
    • 2.調用Singleton的構造函數初始化了一個成員變量,產生了實例,放在另外一處內存空間中
    • 3.將instance對象指向分配的內存空間,執行完這一步纔算真的完成了,instance纔不是null。

    在一個線程裏面是沒有問題的,那麼在多個線程中,JVM作了指令重排的優化就有可能致使問題,由於第二步和第三步的順序是不可以保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回instance,而後使用,就會報空指針。
    從更上一層來講,有一個線程是instance已經不爲null可是仍沒有完成初始化中間狀態,這個時候有一個線程剛恰好執行到第一個if(instance==null),這裏獲得的instance已經不是null,而後他直接拿來用了,就會出現錯誤。
    對於這個問題,咱們使用的方案是加上volatile關鍵字。

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

    volatile的做用:禁止指令重排,把instance聲明爲volatile以後,這樣,在它的賦值完成以前,就不會調用讀操做。也就是在一個線程沒有完全完成instance = new Singleton();以前,其餘線程不可以去調用讀操做。

    • 上面的方法實現單例都是基於沒有複雜序列化和反射的時候,不然仍是有可能有問題的,還有最後一種方法是使用枚舉來實現單例,這個能夠說的比較理想化的單例模式,自動支持序列化機制,絕對防止屢次實例化。
    public enum Singleton {
        INSTANCE;
        public void doSomething() {
    
        }
    }

    以上最推薦枚舉方式,固然如今計算機的資源仍是比較足夠的,餓漢方式也是不錯的,其中懶漢模式下,若是涉及多線程的問題,也須要注意寫法。

    最後提醒一下,volatile關鍵字,只禁止指令重排序,保證可見性(一個線程修改了變量,對任何其餘線程來講都是當即可見的,由於會當即同步到主內存),可是不保證原子性。

    【做者簡介】
    秦懷,公衆號【秦懷雜貨店】做者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。這個世界但願一切都很快,更快,可是我但願本身能走好每一步,寫好每一篇文章,期待和大家一塊兒交流。

    此文章僅表明本身(本菜鳥)學習積累記錄,或者學習筆記,若有侵權,請聯繫做者覈實刪除。人無完人,文章也同樣,文筆稚嫩,在下不才,勿噴,若是有錯誤之處,還望指出,感激涕零~

    相關文章
    相關標籤/搜索