你真的了解單例嗎

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

第一種:

public class SingleTon {

private static SingleTon instance = new SingleTon();

private SingleTon(){}

public static SingleTon get(){
    return instance;
}

}
這種方式簡單直接,實例隨著類加載而加載,很方便,但是卻不友好,有時候我們雖然加載了類,卻沒有使用該類實例的時候,會造成內(nèi)存的浪費,不能達(dá)到懶加載的能力。所以我們可以改進(jìn)成下面這樣:

第二種:

public class SingleTon {

private static SingleTon instance = null;

private SingleTon(){}

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

}
這樣可以達(dá)到懶加載,需要的時候在初始化,但是如果在多線程的情況下是不完全的,那我們會這樣寫:

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;
}

}
如果我們寫到這里就以為很滿足了,那么我只能說太天真了,看似一切完美,但是我們還是要問自己,這樣就絕對的會線程安全嗎?
要回答這個問題,這就不得不說一說對象的創(chuàng)建過程和java虛擬機(jī)的無序性。首先在我們new對象的時候,首先需要在方法區(qū)中去尋找該類的符號引用,如果找不到,說明類還沒有被加載進(jìn)虛擬機(jī),所以需要通過類加載器先裝載該類,通過加載,驗證,準(zhǔn)備,解析,初始化等操作,然后為對象在堆上開辟內(nèi)存空間(1),對象初始化操作(2),然后在將棧上的引用指向該對象內(nèi)存地址(3)。重點就在這,由于虛擬機(jī)的無序性,可能會造成執(zhí)行的順序并不是按照123進(jìn)行的,也可能是按照132的執(zhí)行順序,結(jié)果就是引用先指向?qū)ο蟮刂?,然后對象在進(jìn)行初始化等操作,這是由于線程的可見性造成的,所以為了保證變量instance的線程之間的可見性,我們需要將instance變量進(jìn)行volatile修飾來解決instance的可見性問題。(關(guān)于java虛擬機(jī)的無序性和volatile的內(nèi)存語義,涉及到了java內(nèi)存模型的層面,這里暫時不過多分析,后面會單獨進(jìn)行講解)。

所以正確的寫法是這樣:

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;
    }
}

}
那么到此我們就可以滿足了嗎?當(dāng)然不能。

這里不可避免的需要對volatile進(jìn)行解釋一下了,volatile在《深入理解java虛擬機(jī)》中有一下幾層含義:

1,被volatile修飾的變量,保證了該變量對其他線程的可見性,;

2,禁止指令重排序,虛擬機(jī)會通過插入很多讀寫內(nèi)存屏障,來保證處理器不會亂序執(zhí)行,但是也會造成編譯器不會對代碼進(jìn)行優(yōu)化(java內(nèi)存模型會最大限度的保證程序并行執(zhí)行),對效率有一定影響。

那么我們在不使用volatile的前提下如何優(yōu)化呢,下面給出某大牛的寫法:

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的存在虛擬機(jī)無法優(yōu)化temp是否為空,instance在賦值之前保證了對象已經(jīng)初始化完成??吹竭@里明顯感覺到水很深啊。

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

第三種:

public class SingleTon {

private SingleTon() {
}

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

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

}
這里我們通過靜態(tài)內(nèi)部類來完成,是不是很妙,我們無需枷鎖,外部類的加載不會造成內(nèi)部類同時加載的,只有調(diào)用了get方法時才會加載內(nèi)部類,創(chuàng)建對象,集前兩種方法的優(yōu)點于一身。

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

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

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

public enum SingleTon {
intstance;
}
是不是有點意外了,居然最簡單最安全的是枚舉,至于枚舉是如何做到反射和反序列化時依然安全的可以看鏈接:

https://blog.csdn.net/gavin_dyson/article/details/70832185

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 最新在閱讀《Android源碼設(shè)計模式解析與實戰(zhàn)》一書,我覺得寫的很清晰,每一個知識點都有示例,通過示例更加容易理...
    慕涵盛華閱讀 618評論 0 3
  • 前言 本文主要參考 那些年,我們一起寫過的“單例模式”。 何為單例模式? 顧名思義,單例模式就是保證一個類僅有一個...
    tandeneck閱讀 2,624評論 1 8
  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,790評論 11 349
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,652評論 18 399
  • 無意間見一個群里討論《每一天》這部電影,出于好奇心,窩在國慶節(jié)期間依靠一些零碎時間,看完這部電影,它是...
    天天向上8888閱讀 633評論 2 4

友情鏈接更多精彩內(nèi)容