1. 程式人生 > >你真的瞭解單例嗎

你真的瞭解單例嗎

    又到了一個老生常談的話題,單例模式,可能在面試時我們也經常會遇到,但是看似很簡單的問題,卻能看出一個人對單例理解的深度。要寫一個單例,首先需要讓構造器私有,還需要對外提供一個可以獲取單例的一個入口,通常我們可能會這樣寫:

第一種:

public class SingleTon {

    private static SingleTon instance = new SingleTon();

    private SingleTon(){}

    public static SingleTon get(){
        return instance;
    }
}

這種方式簡單直接,例項隨著類載入而載入,很方便,但是卻不友好,有時候我們雖然載入了類,卻沒有使用該類例項的時候,會造成記憶體的浪費,不能達到懶載入的能力。所以我們可以改進成下面這樣:

第二種:

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon(){}

    public static SingleTon get(){
        if (instance == null){
            instance =new SingleTon();
        }
        return instance;
    }
}

這樣可以達到懶載入,需要的時候在初始化,但是如果在多執行緒的情況下是不完全的,那我們會這樣寫:

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon(){}

    public static synchronized SingleTon get(){
        if (instance == null){
            instance =new SingleTon();
        }
        return instance;
    }
}

雖然這樣安全了,但是鎖的粒度還是比較大,所以為了減小鎖的粒度我們還會這樣寫:

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon() {
    }

    public static SingleTon get() {
        if (instance == null) {
            synchronized (SingleTon.class) {
                if (instance == null) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}
    如果我們寫到這裡就以為很滿足了,那麼我只能說太天真了,看似一切完美,但是我們還是要問自己,這樣就絕對的會執行緒安全嗎?

    要回答這個問題,這就不得不說一說物件的建立過程和java虛擬機器的無序性。首先在我們new物件的時候,首先需要在方法區中去尋找該類的符號引用,如果找不到,說明類還沒有被載入進虛擬機器,所以需要通過類載入器先裝載該類,通過載入,驗證,準備,解析,初始化等操作,然後為物件在堆上開闢記憶體空間(1),物件初始化操作(2),然後在將棧上的引用指向該物件記憶體地址(3)。重點就在這,由於虛擬機器的無序性,可能會造成執行的順序並不是按照123進行的,也可能是按照132的執行順序,結果就是引用先指向物件地址,然後物件在進行初始化等操作,這是由於執行緒的可見性造成的,所以為了保證變數instance的執行緒之間的可見性,我們需要將instance變數進行volatile修飾來解決instance的可見性問題。(關於java虛擬機器的無序性和volatile的記憶體語義,涉及到了java記憶體模型的層面,這裡暫時不過多分析,後面會單獨進行講解)。

所以正確的寫法是這樣:

public class SingleTon {

    private static volatile SingleTon instance = null;

    private SingleTon() {
    }

    public static SingleTon get() {
        if (instance == null) {
            synchronized (SingleTon.class) {
                if (instance == null) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }
}

我們還可以改成成一個類,專門生成單例:

public abstract class Singleton<T> {
    private volatile T mInstance;

    protected abstract T create();

    public final T get() {
        synchronized (this) {
            if (mInstance == null) {
                mInstance = create();
            }
            return mInstance;
        }
    }
}

那麼到此我們就可以滿足了嗎?當然不能。

這裡不可避免的需要對volatile進行解釋一下了,volatile在《深入理解java虛擬機器》中有一下幾層含義:

1,被volatile修飾的變數,保證了該變數對其他執行緒的可見性,;

2,禁止指令重排序,虛擬機器會通過插入很多讀寫記憶體屏障,來保證處理器不會亂序執行,但是也會造成編譯器不會對程式碼進行優化(java記憶體模型會最大限度的保證程式並行執行),對效率有一定影響。

那麼我們在不使用volatile的前提下如何優化呢,下面給出某大牛的寫法:

public class SingleTon {

    private static SingleTon instance = null;

    private SingleTon() {
    }

    public static SingleTon get() {
        if (instance == null) {
            synchronized (SingleTon.class) {
                if (instance == null) {
                    SingleTon temp = null;
                    try {
                        temp = new SingleTon();
                    } catch (Exception e) {

                    }
                    if (temp != null)
                        instance = temp;
            }
        }
        return instance;
    }
}

看似無用的程式碼卻大有用處,try的存在虛擬機器無法優化temp是否為空,instance在賦值之前保證了物件已經初始化完成。看到這裡明顯感覺到水很深啊難過

       前面其實大概分為兩種,餓漢式和懶漢式,那有沒有既執行緒安全寫法簡單,又能懶載入呢?

第三種:

public class SingleTon {

    private SingleTon() {
    }

    private static class SingleHolder {
        private static SingleTon instance = new SingleTon();
    }

    public static SingleTon get() {
        return SingleHolder.instance;
    }
}

這裡我們通過靜態內部類來完成,是不是很妙,我們無需枷鎖,外部類的載入不會造成內部類同時載入的,只有呼叫了get方法時才會載入內部類,建立物件,集前兩種方法的優點於一身大笑

但是到這裡我們又要分析了,以上寫法到底完全不,如果通過反射或者反序列化還能保證是單例嗎?

    當然不可能,在反射面前,一切都是小兒科了,這種寫法可阻止不了反射,反序列化也不行,你必須重寫readReslove方法,返回當前例項,不然就是多個例項了,

    那到底有沒有絕對安全的單例啊,我們是不是都快絕望了,別急,放大招:

public enum  SingleTon {
    intstance;
}

是不是有點意外了,居然最簡單最安全的是列舉,至於列舉是如何做到反射和反序列化時依然安全的可以看連結:

好了,單例到此介紹完畢,看完這些你對單例模式真的瞭解了嗎?