單例模式之進(jìn)化心路

前言

????單例模式應(yīng)該是編程中使用最多的設(shè)計模式之一,寫好單例模式往往能體現(xiàn)一個程序員的基本功。單例模式看似簡單,但是要將其設(shè)計得高效、安全、優(yōu)雅,還是需要考慮很多細(xì)節(jié)之處。本文將從最簡單的單例模式開始分析,從簡單-高效-安全-優(yōu)雅逐步演化。

非線程安全版

????簡單版只需實現(xiàn)單例功能,并不考慮其他因素。同時簡單版分餓漢和懶漢模式,至于何為餓漢何為懶漢,接下來將逐一介紹。

1. 餓漢模式

????何為餓漢模式,簡單形象來說就是像餓漢一樣,很饑餓,需要立馬得到食物。在代碼中體現(xiàn)就是,一開始就得把單例對象創(chuàng)建出來,需要時立馬就能拿到,不需要其他額外操作。代碼如下所示:

public class Singleton {
    private Singleton() {}
    // 餓漢模式
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
}

????從上面的代碼可以看出,餓漢模式相當(dāng)簡單,同時,這種方式也存在一些問題,一是靜態(tài)成員變量instance 在一開始就會被創(chuàng)建出來,即使整個運行階段都未使用,也會占用內(nèi)存;另一個是線程不安全,在多線程環(huán)境下會創(chuàng)建多個對象。針對這兩個問題,下面會逐一解決。

2. 懶漢模式

????和餓漢模式相反,懶漢模式如同其名,在對象需要使用時才創(chuàng)建出來,避免餓漢模式導(dǎo)致的內(nèi)存浪費。代碼如下所示:

public class Singleton {
    private Singleton() {}
    // 懶漢模式
    private static Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

????可以看到,懶漢模式也很簡單,instance 初始化為null,只有在需要使用時才去創(chuàng)建對象,解決了餓漢模式內(nèi)存浪費問題,但是線程安全問題依舊沒解決。下一小節(jié)將討論線程安全單例模式如何實現(xiàn)。

線程安全版

????在編寫多線程程序時,線程安全是最基本也是最重要的考慮之一。接下來,我們將討論線程安全版本單例模式的幾種實現(xiàn)方式。

1. 簡單粗暴法

????大家很容易想到,要想保證線程安全那直接在方法前加鎖使其成為同步方法,那么在多線程同時調(diào)用方法時,需要排隊執(zhí)行同步塊,保證了線程的安全。這種實現(xiàn)方式如下:

public class Singleton {
    private Singleton() {}
    private static Singleton instance = null;

    // 同步方法
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

????上訴方法在getInstance前加synchronized 關(guān)鍵字保證方法的同步調(diào)用,實現(xiàn)了線程安全版的單例模式,這種方法簡單而且也達(dá)到了目的,但是缺陷是效率極低,每次調(diào)用方法獲取單例對象時都得加鎖,如果調(diào)用頻繁則對程序性能影響很嚴(yán)重。針對效率問題,下面會給出解決方法。

2. Double CheckLock(DCL)法

????既然每次加鎖會導(dǎo)致效率問題,那我們可以考慮是否可以縮小鎖的粒度,在必須加鎖的情況下才加鎖,其他情況直接饒過加鎖操作。如下所示:

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

????大家注意到,只有當(dāng)單例對象為空時,才進(jìn)入同步代碼塊,不為空時,直接返回對象,這種方式對比上一種實現(xiàn)方式極大提高了程序的性能。同時,這里有兩次判空操作,有些同學(xué)可能疑惑為什么需要兩次,這里解釋下:第一次判空,相信大家都能理解,即當(dāng)對象為空時才進(jìn)入同步塊;第二次判空是為了避免這種情況---當(dāng)多個線程同時通過了第一次判空,進(jìn)入同步塊,如果沒有第二次判斷操作,則會創(chuàng)建出多個單例對象。這種方法解決了性能問題,但是依舊有不足之處,那內(nèi)存浪費和線程安全都解決了,那存在什么不足呢?這種方式真的就線程安全了嗎?no,這里還涉及到比較底層的細(xì)節(jié)---jvm編譯器指令重排。何為指令重排,相信大家都了解,如上面一行代碼instance = new Singleton();編譯成jvm指令可能如下:

address = allocate(); // 1. 給對象分配內(nèi)存空間
ctorInstance(address); // 2. 初始化對象
instance = address; // 3. 將instance指向?qū)ο?

如果編譯器不搞事,正常情況下,按上面的jvm指令執(zhí)行不會出現(xiàn)線程安全問題,但是如果jvm按照自己的優(yōu)化算法將上訴指令進(jìn)行優(yōu)化,就會出現(xiàn)下面這種情況:

address = allocate(); // 1. 給對象分配內(nèi)存空間
instance = address; // 2. 將instance指向?qū)ο?ctorInstance(address); // 3. 初始化對象

那如果按照上面的指令順序運行會出現(xiàn)什么問題呢,為了能夠形象的解釋這一問題,我把上訴執(zhí)行替換到j(luò)ava代碼中,如下:

public class Singleton {
    private Singleton() {}
    private static Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) {  // first check
            synchronized (Singleton.class) {
                if (instance == null) { // second check
                   address = allocate(); // 1. 給對象分配內(nèi)存空間
                   instance = address; // 2. 將instance指向?qū)ο?(注意,執(zhí)行完這條命令后,instance將不為null)
                   ctorInstance(address); // 3. 初始化對象
                }
            }
        }
        return instance;
    }
}

按照上訴代碼的執(zhí)行邏輯,假設(shè)有兩個線程a、b。當(dāng)a線程通過了兩次check,執(zhí)行完instance = address后,此時instance的狀態(tài)是不為null且未被初始化,假設(shè)此時b線程運行到第一次判空檢查發(fā)現(xiàn)instance已不為null,那直接返回instance,那么問題就來了,雖然此時instance不為null,但是其并未被初始化,如果直接拿來使用必然會導(dǎo)致程序出錯。因此出現(xiàn)線程安全問題。那么如何解決指令重排的問題呢?這里引入volatile關(guān)鍵字防止jvm指令重排,如下所示:

public class Singleton {
    private Singleton() {}
    private volatile static Singleton instance = null;  // 防止指令重排
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

3. 靜態(tài)內(nèi)部類實現(xiàn)模式

????前面的方法已經(jīng)解決了內(nèi)存浪費、效率及線程安全問題,那還有沒有更加優(yōu)雅的方法呢?答案是肯定的,這里引入靜態(tài)內(nèi)部類實現(xiàn)方法,即能解決上訴所有問題也能體現(xiàn)出代碼的優(yōu)雅感,如下:

public class Singleton {
    private Singleton() {}
    private static class Inner {
        private static final Singleton instance = new Singleton();
    }
     
    public static Singleton getInstance() {
        return Inner.instance;
    }
}

????內(nèi)部類是延時加載的,也就是說只會在第一次使用時加載,不使用就不加載。使用內(nèi)部類可以在不加鎖的情況下,保證線程安全,因此用內(nèi)部類能夠優(yōu)雅的實現(xiàn)單例模式。

小結(jié)

????本文從內(nèi)存、性能、安全角度出發(fā),對單例模式的實現(xiàn)逐步演進(jìn),在保證這三者的前提下最終討論出一種優(yōu)雅的實現(xiàn)方法。如有不足之處,歡迎交流討論,謝謝。

?著作權(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)容

  • 男人嫌女人事多,女人嫌男人不解風(fēng)情,于是,男人和女人之間多了爭吵,而男人又是見吵架就頭大的物種,若想避免頻繁爭吵,...
    紅豆印跡閱讀 1,056評論 0 0
  • 今天是周六總的來說今天的效率是很低的,時間的利用率沒有到位。大部分的時間都浪費在了娛樂或者等待上,并沒有把時間放在...
    龐校長閱讀 270評論 0 1
  • 李農(nóng)乘車將欲行 吾獨店中暗傷離 桃花潭水三千尺 不及我倆姐妹情
    紅色陽光閱讀 410評論 2 3

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