單例模式探討

概述

單例模式是一種對象創建模式,用於產生一個對象的具體實例,它可以確保系統中一個類只產生一個實例。該模式能夠帶來兩大好處:

  1. 對於頻繁使用的對象,可以減小new操作花費的開銷;
  2. 由於new的次數減少,將會減輕GC壓力,縮短GC停頓時間。

單例模式主要有懶漢式和餓漢式兩種實現形式。

餓漢式單例模式

餓漢式會先行創建出instance實例以保證線程安全,但是無法控制該實例的創建時機。如下代碼爲餓漢式單例模式的實現,當類的內部有個靜態變量STATUS時,在任何地方引用該變量都會導致instance實例的創建,但是由於它已經先創建好了實例,在需要的時候直接返回該實例,因此它是線程安全的。

public class HungrySingleton {
    public static int STATUS = 1;
    private static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

懶漢式單例模式

懶漢式單例模式在多線程的環境下會出現問題,我們通常採用加鎖的方式避免多個線程同時實例化。並且從代碼優化的角度考慮,設置了雙重檢查鎖。根據以下懶漢式單例模式在多線程環境下的經典實現方式進行逐步解析:

public class LazySingleton {
    private volatile static LazySingleton instance = null;  // 1

    private LazySingleton() {  // 2

    }

    public static LazySingleton getInstance() {  // 3
        if (instance == null) {  // 4
            synchronized (LazySingleton.class) {  // 5
                if (instance == null) {  // 6
                    instance = new LazySingleton(); // 7
                }
            }
        }
        return instance;  //8
    }
}
1、private static修飾instance的原因

標記1處使用private static對instance進行修飾。由於單例模式需要保證全局只有一個實例,因此使用private能夠避免從外部直接創建實例,使用static能夠讓instance被所有的類實例所共享,在內存中只有一個副本,當且僅當在類初次加載時會被初始化。

2、使用private修飾構造方法的原因

標記2處使用private對構造方法進行修飾。目的是避免該類在外部創建實例,包括禁止了通過子類繼承該類並通過構造函數創建Singleton實例的可能性。

3、使用public修飾getInstance方法的原因

標記3處使用public修飾getInstance。由於單例模式封閉了其它所有創建Singleton實例的入口,因此需要給出一個公開的外部接口,使得內部僅能夠通過該接口獲得Singleton實例,確保Singleton實例的唯一性。

4、雙重檢查機制

雙重檢查機制主要是從性能的角度進行考慮的。雙重檢查在標記4和標記6兩處對instance進行了判null操作,原因是當instance爲null時,我們纔對它進行實例化。如果沒有標記4處的判null操作,則getInstance方法在每次被調用時,都會直接加鎖,這極大地影響了多線程環境下的性能。因此我們在調用getInstance方法時,先進行一次判null,只有當前判null爲true時纔會加鎖,否則直接返回實例。
假設線程A運行至標記4處instance爲null,於是到達標記5處,而線程B此時恰好完成了對instance實例化的操作並釋放了鎖,線程A獲取鎖後仍需進行第二次判null操作纔是正確的邏輯,這就是需要雙重檢查的原因。

5、synchronized鎖住的是什麼

標記5處使用的synchronized對Singleton.class進行了加鎖操作。該鎖是類鎖,鎖住的是類對象,與在靜態方法上加上關鍵字的效果是一樣的。標記5處的鎖使得在多線程環境中,同一時刻只有一個線程能夠進入標記6和標記7處並嘗試對instance進行實例化。

6、volatile關鍵字的作用

標記1處使用volatile修飾instance。volatile主要保證的是可見性,以及禁止指令重排序。可見性是指線程之間的可見性,一個線程修改的狀態對另一個線程是立即可見的。重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行重新排序的一種手段。在以上單例模式的實現中,標記7處的代碼創建了一個Singleton實例,但是該行代碼並不是一個原子操作,可以被分爲以下三個步驟:

memory = allocate();  // 1.分配對象的內存空間
ctorInstance(memory); // 2.初始化對象
instance = memory;  // 3.將instance指向剛分配的內存地址

如果我們不使用volatile修飾instance,也就意味着JVM會對以上步驟進行優化,對指令進行重排序。由於2和1之間有memory的依賴關係所以不會進行重排序,但是3和2之間沒有依賴關係,可能會進行重排序,例如:

memory = allocate();  // 1.分配對象的內存空間
instance = memory;  // 3.將instance指向剛分配的內存地址
ctorInstance(memory); // 2.初始化對象

當線程A在運行至標記7,且正在經歷重排序後的第二個步驟:instance = memory時,instance已經指向了剛分配的地址,但是由於發生了指令重排,該內存地址指向的對象還沒有被完全初始化(可能正在被初始化);與此同時線程B來到標記4處,由於instance已經指向了一個地址,所以判null爲false,此時會直接返回instance。但是此時返回的instance還正在初始化當中,可能並沒有完全被初始化,於是導致程序出錯。而如果使用volatile對instance進行修飾,則會禁止指令重排,不會出現如上所述的情況。
綜上所述,如果不使用volatile對instance進行修飾,由於synchronized的存在,程序一定不會創建出兩個實例,但是有可能會創建出半個實例。

優化單例模式

在生產環境中漸漸摒棄了雙重檢查鎖這種複雜且醜陋的實現方式,而是採用一種融合了懶漢式和餓漢式優點的實現方式:

public class StaticSingleton {
    private static class SingletonHolder {
        private static StaticSingleton instance = new StaticSingleton();
    }

    private StaticSingleton() {

    }

    public static StaticSingleton getInstance() {
        return SingletonHolder.instance;
    }
}

首先,它實現了餓漢式的優點,在內部類中創建了instance實例,沒有對getInstance進行加鎖卻保證了線程安全;其次,它巧妙地使用了內部類和類的初始化方式,將SingletonHolder聲明爲private,使得外部無法訪問到它,當且只有第一次調用getInstance這一方法時,纔會對該類進行初始化並返回instance。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章