JAVA_多執行緒_單例模式

動物園裡的一隻程式猿發表於2017-09-03

 

這篇是入職之後的第二篇了,上一篇我簡單介紹了一下LOCK裡面的類的方法,感興趣的話可以去了解一下,以後堅持每週至少會更新一篇關於多執行緒方面的文章,希望博友們可以一起加油成長。

這篇主要的內容是單例模式在多執行緒環境下的設計,這篇算是比較重要的內容,我會進行文字和程式碼的共同說明來講解記錄

1、立即載入(餓漢模式)

說到標題,有人會說什麼是立即載入呢?立即載入就是使用類的時候已經將物件建立完畢了,比如說直接new例項化物件。也就是在呼叫方法之前,例項已經被建立了

  

public class MyObject {
    private static MyObject myObject = new MyObject();
    private MyObject(){

    }
    public static MyObject getInstance(){
        return myObject;
    }
}

 

看這段程式碼,他的缺點是不能有其他例項變數。外部的使用者需要使用MyObject例項的時候,只能通過getInstance方法,另外假如沒有用到這個例項的時候,他已經建立了出現,會有資源浪費的情況出現的。還有因為getInstance方法沒有同步,所以有可能出現非執行緒安全的問題。

2、延遲載入(懶漢模式)

延遲載入就是在呼叫要使用的那個方法(假如MyObject方法)的時候例項才會被建立,實現方法就是在MyObject方法裡面進行new例項化。

  

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
    public static MyObject getInstance(){
        if(myObject==null){
            myObject = new MyObject();
        }
        return myObject;
    }
}

 

此程式碼雖然取得了一個物件,沒毛病。單例!但是如果在多執行緒情況下,就會取出多個例項的情況,這個是與單例模式的初衷背道而馳的。

  

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
    public static MyObject getInstance(){
        if(myObject==null){
            try {
                Thread.sleep(3000);
                myObject = new MyObject();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return myObject;
    }
}

可以試著自己多建立幾個執行緒,執行一下這段程式碼,發現在多執行緒環境在建立出來了多個例項(可以列印物件的hashcode值進行比較)

那我們應該怎麼去解決這個問題呢?

3、延遲載入(懶漢模式)——synchronized

  

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
   synchronized public static MyObject getInstance(){
        if(myObject==null){
            try {
                Thread.sleep(3000);
                myObject = new MyObject();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return myObject;
    }
}

這樣OK?看上去是的,是解決了得到了相同的例項,但是見到了synchronized這個東西,你不得猶豫一下麼?加在了整個方法上啊,如果這個方法設計到比如說很多的過程或者運算,下一個執行緒想要取得物件,不是要等到程式設計師找到女朋友才行麼。也就是要等到上一個執行緒釋放鎖之後,才可以繼續進行。

有人說,我不加全部,我加部分不行麼?

  

   public static MyObject getInstance(){
        synchronized (MyObject.class) {
            if (myObject == null) {
                try {
                    Thread.sleep(3000);
                    myObject = new MyObject();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return myObject;
    }
}

你仔細看,有啥大的變化麼,並沒有吧。因為效率還是一樣的低低低低。每次我呼叫getIstance的時候是不是還要同步啊,所以太大變化啦

然後機智的我想出了這樣的方法

  

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
   public static MyObject getInstance(){

        if (myObject == null) {
            try {
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    myObject = new MyObject();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return myObject;
    }
}

我只載入了需要建立物件的那個關鍵地方,看到了麼,這樣效率大大滴提升了。但是,重點來了,我靠,列印出來了兩個不同物件,列印出來的物件hashcode值不一樣了啊,不是一個物件了,因為兩個執行緒都進入了if語句內,之後沒有在進行判斷,所以建立了兩個物件。我單例什麼呢?所以這個方法也pass

那我到底該怎麼辦呢?彆著急,DCL雙重檢查所機制,不廢話直接看程式碼

  

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
   public static MyObject getInstance(){

        if (myObject == null) {
            try {
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    if(myObject == null) {
                        myObject = new MyObject();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return myObject;
    }
}

哇,終於有方法可以實現單例模式在多執行緒環境下的正常工作了,哈哈哈哈哈,但是但是那你們就打錯特錯了。看如下分析

從JVM的角度來說,怎麼建立一個物件呢?第一是申請一塊記憶體,呼叫構造方法進行初始化操作,第二是分配一個指標指向這塊記憶體。這兩個操作誰在前面,誰在後面JVM並不會管它。那麼就存在這麼一種情況,JVM是先開闢出一塊記憶體,然後把指標指向這塊記憶體,最後呼叫構造方法進行初始化。

執行緒A開始建立MyObject的例項,此時執行緒B呼叫了getInstance()方法,首先判斷MyObject是否為null。假設A已經把MyObject指向了那塊記憶體,只是還沒有呼叫構造方法,因此B檢測到MyObject不為null,於是直接把MyObject返回了——問題出現了,儘管MyObject不為null,但它並沒有構造完成結束。此時,如果B在A將MyObject構造完成之前就是用了這個例項,程式就會出現錯誤了!(其實在private static MyObject myObject;  改為   private volatile static MyObject myObject;  就不會發生這樣的結果了。被volatile修飾的寫變數不能和之前的讀寫程式碼調整,這裡我們當做這個關鍵字不存在,以後會有專門的篇幅去詳細講解這個關鍵字的,這個關鍵字的坑有許多,我們慢慢踩)

那我們到底的咋整啊?

 

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
   public static MyObject getInstance(){

        if (myObject == null) {
            MyObject my;
            synchronized (MyObject.class) {
                my = myObject;
                if (my == null) {
                    synchronized (MyObject.class) {
                        if (my == null) {
                            my = new MyObject();
                            }
                        }
                        myObject = my;
                    }
                }
            }
            return myObject;
        }
    }

  

我們在第一個同步塊裡面建立一個臨時變數,然後使用這個臨時變數進行物件的建立,並且在最後把myObject指標臨時變數的記憶體空間。寫出這種程式碼基於以下思想,即synchronized會起到一個程式碼遮蔽的作用,同步塊裡面的程式碼和外部的程式碼沒有聯絡。因此,在外部的同步塊裡面對臨時變數my進行操作並不影響myObject,所以外部類在myObject=my;之前檢測myObject的時候,結果myObject依然是null。

由於同步塊的釋放保證在此之前——也就是同步塊裡面——的操作必須完成,但是並不保證同步塊之後的操作不能因編譯器優化而調換到同步塊結束之前進行。因此,編譯器完全可以把myObject=my;這句移到內部同步塊裡面執行。又錯了。

4、內部類實現方式

  

public class MyObject_inner {
    private static class MyObjectHandler{
        private static MyObject_inner myObject_inner = new MyObject_inner();
    }
    private MyObject_inner(){}
    public static MyObject_inner getInstance(){
        return MyObjectHandler.myObject_inner;
    }
}

在這一版本的單例模式實現程式碼中,我們使用了Java的靜態內部類。這一技術是被JVM明確說明了的,因此不存在任何二義性。在這段程式碼中,因為Myobject_inner沒有static的屬性,因此並不會被初始化。直到呼叫getInstance()的時候,會首先載入MyObjectHandler類,這個類有一個static的MyObject_inne例項,因此需要呼叫MyObject_inne的構造方法,然後getInstance()將把這個內部類的myobject_inner返回給使用者。由於這個myobject_inner是static的,因此並不會構造多次。

由於MyObjectHandler是私有靜態內部類,所以不會被其他類知道,同樣,static語義也要求不會有多個例項存在。並且,JSL規範定義,類的構造必須是原子性的,非併發的,因此不需要加同步塊。同樣,由於這個構造是併發的,所以getInstance()也並不需要加同步。

但是這種情況完全是對的麼?假如遇到序列化的物件呢?會是什麼樣的結果?

 

5、序列化與反序列化的單例模式的實現

靜態內部類可以達到執行緒安全的問題,但是如果遇到序列化物件的時候,使用預設的方式執行得到的結果還是多例的。

具體是為什麼在序列化的時候不是單例的,本人我掌握的不太好,後續會把其中涉及到的知識補充之後,在完善此篇文章。

序列化會通過反射呼叫無引數的構造方法建立一個新的物件。解決的方式就是在implements Serializable這個方法裡面加上這段程式碼

  
import java.io.Serializable;

public class MyObject_inner implements Serializable{
    private static final long seriaVersionUID = 8899L;
    private static class MyObjectHandler{
        private static MyObject_inner myObject_inner = new MyObject_inner();
    }
    private MyObject_inner(){}
    public static MyObject_inner getInstance(){
        return MyObjectHandler.myObject_inner;
    }

    private Object readResolve(){
        return MyObjectHandler.myObject_inner;
    }
}

  

6、靜態程式碼塊實現單例模式

  

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

  

7、列舉方法實現單例模式

  

public class MyObject_enum {
    public enum EnumSingleton{
        Instance;
        private MyObject_enum instance;
        EnumSingleton(){
            instance = new MyObject_enum();
        }
        public MyObject_enum getInstance(){
            return instance;
        }
    }
}

獲取資源的方式很簡單,只要 EnumSingleton.INSTANCE.getInstance() 即可獲得所要例項。下面我們來看看單例是如何被保證的:

首先,在列舉中我們明確了構造方法限制為私有,在我們訪問列舉例項時會執行構造方法,同時每個列舉例項都是static final型別的,也就表明只能被例項化一次。在呼叫構造方法時,我們的單例被例項化。也就是說,因為enum中的例項被保證只會被例項化一次,所以我們的INSTANCE也被保證例項化一次。列舉也提供了序列化機制。所以單元素的列舉型別已經成為了實現單例模式的最佳方法

  

這篇文章到此暫時先告一段落,裡面有一點設計的序列化與反序列化實現單例模式我以後會在繼續的更新補充進去的。 歡迎各位博友批評指正

  
 

 

  

  

  

相關文章