你真的懂單例模式嗎

在面試中咱們常常會被問到:「你熟悉單例模式嗎?請手寫一個單例模式的實現?單例模式的應用有哪些……」。有關單例模式的問題比比皆是,在面試中也是很是常見的。html

所謂單例模式就是確保一個類只有一個實例,並對外提供該實例的全局訪問點java

實現

類圖以下: git

singleton
解讀:

  • 實現單例模式的思路:
    • 在類中有一個自身的變量(這個變量能夠在使用時建立,也可使用前建立);
    • 確保全局只有該變量的一個實例;
    • 對外提供一個訪問該變量的公共方法;
  • 對照上面的實現思路,可知實現步驟分爲三步,具備如下特色:
    • 私有的靜態變量:加 static 關鍵字,至關因而一個常量,體現該實例是獨一份的特色;
    • 構造函數私有化:目的是不容許其餘類執行 new 操做,即不容許其餘類建立該類,即也能保證全局只有一個該變量的實例;
    • 提供靜態的全局訪問點:訪問該類的私有變量,因私有變量被 static 關鍵字修飾,因此獲取該變量的公共方法也必須是 static 修飾的;

懶漢式-線程不安全

實現代碼

public class LazySingleton {
    // 構造函數私有化
    private LazySingleton(){
        System.out.println("當前線程名稱: " + Thread.currentThread().getName() + "\t 我是構造方法...");
    }

    // 私有的靜態變量
    private static LazySingleton lazySingleton;

    // 提供靜態的全局訪問點
    public static LazySingleton getSingleton() {
        if (lazySingleton == null) {
            lazySingleton = new LazySingleton();
        }
        System.out.println(lazySingleton); // 打印當前對象的惟一標識
        return lazySingleton;
    }
}
複製代碼

測試代碼

public class TestSingletons {
    public static void main(String[] args) {
        // 單線程場景下,直接調用
        LazySingleton.getSingleton();
        LazySingleton.getSingleton();
        LazySingleton.getSingleton();
        LazySingleton.getSingleton();

        // 多線程場景
        /*for (int i = 0; i < 10; i++) { new Thread(() -> { LazySingleton.getSingleton(); }, String.valueOf(i)).start(); }*/
    }
}
複製代碼
  1. 打開單線程場景代碼並註釋多線程場景代碼:
  • 運行結果以下:
當前線程名稱: main	 我是構造方法...
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
singleton.LazySingleton@4554617c
複製代碼
  • 結果分析: 由結果發現整個過程只構造了一次,這個變量的惟一標識爲4554617c,說明單線程場景下是沒有問題的
  1. 打開多線程場景代碼並註釋單線程場景代碼:
  • 運行結果以下:
當前線程名稱: 2	 我是構造方法...
當前線程名稱: 8	 我是構造方法...
當前線程名稱: 4	 我是構造方法...
singleton.LazySingleton@ae526cf
當前線程名稱: 6	 我是構造方法...
當前線程名稱: 0	 我是構造方法...
singleton.LazySingleton@134f6cee
當前線程名稱: 3	 我是構造方法...
當前線程名稱: 9	 我是構造方法...
singleton.LazySingleton@6e154e44
當前線程名稱: 5	 我是構造方法...
當前線程名稱: 7	 我是構造方法...
當前線程名稱: 1	 我是構造方法...
singleton.LazySingleton@2fd04fd1
singleton.LazySingleton@67c084e5
singleton.LazySingleton@47e3e4b5
singleton.LazySingleton@1b9c704e
singleton.LazySingleton@21279f82
singleton.LazySingleton@2ceb2de
singleton.LazySingleton@14550b42
複製代碼
  • 結果分析: 屢次運行結果不一致,實例的惟一標識各不相同,也就是構造了十次,每次都會產生一個新的實例。這說明該實現方式在多線程場景下是沒法保證線程安全的

懶漢式-線程安全

在上一個實現方式的基礎上加以改進,以求保證在多線程條件下能夠達到線程安全的目的。基於這種思路,能夠得出懶漢式的另一種實現方式——線程安全的實現方式。github

實現代碼

public class LazySafeSingleton {
    // 構造方法私有化
    private LazySafeSingleton(){
        System.out.println("當前線程名稱: " + Thread.currentThread().getName() + "\t 我是構造方法...");
    }

    // 私有的靜態變量
    private static LazySafeSingleton lazySafeSingleton;

    // 提供同步的靜態全局訪問點
    public synchronized static LazySafeSingleton getSingleton() {
        if (lazySafeSingleton == null) {
            lazySafeSingleton = new LazySafeSingleton();
        }
        System.out.println(lazySafeSingleton); // 打印當前對象的惟一標識
        return lazySafeSingleton;
    }
}
複製代碼

該方式與第一種方式只有一點區別:在提供的全局訪問點,即獲取實例對象的公共方法加了同步鎖,保證同一時刻,只能由一個線程訪問 getSingleton() 方法。面試

延伸閱讀: java synchronized詳解安全

測試代碼

public class TestSingletons {
    public static void main(String[] args) {
        // 單線程場景
        LazySafeSingleton.getSingleton();
        LazySafeSingleton.getSingleton();
        LazySafeSingleton.getSingleton();
        LazySafeSingleton.getSingleton();

        // 多線程場景
        /*for (int i = 0; i < 10; i++) { new Thread(() -> { LazySafeSingleton.getSingleton(); }, String.valueOf(i)).start(); }*/
    }
}
複製代碼
  1. 單線程場景:
  • 運行結果:
當前線程名稱: main	 我是構造方法...
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
singleton.LazySafeSingleton@4554617c
複製代碼
  • 結果分析: 只構造一次,單線程場景下是沒有問題的。
  1. 多線程場景下:
  • 運行結果:
當前線程名稱: 0	 我是構造方法...
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
singleton.LazySafeSingleton@134f6cee
複製代碼
  • 結果分析: 屢次運行後的結果,會發現都只會構造一次。

雙重鎖校驗-線程安全

分析上一個實現 getSingleton() 方法上加了 synchronized 關鍵字修飾,雖然能保證同一時刻只能由一個線程訪問,保證了多線程場景下的一致性,可是這也會帶了另一個問題:併發性下降。因此,接着對懶漢式-線程安全進行改進。bash

實現代碼

public class DCLSingleton {
    // 構造方法私有化
    private DCLSingleton(){
        System.out.println("當前線程名稱: " + Thread.currentThread().getName() + "\t 我是構造方法...");
    }

    // 靜態私有變量
    private volatile static DCLSingleton dclSingleton;

    // 提供靜態全局訪問點
    public static DCLSingleton getDclSingleton() {
        if (dclSingleton == null) {
            synchronized (DCLSingleton.class) {
                if (dclSingleton == null) {
                    dclSingleton = new DCLSingleton();
                }
            }
        }
        System.out.println(dclSingleton); // 打印當前對象的惟一標識
        return dclSingleton;
    }
}
複製代碼

對比上一個實現方式,會發現有兩處區別:多線程

  1. 在私有變量上面加了volatile關鍵字。 緣由是:dclSingleton = new DCLSingleton();編譯成字節碼後分爲三個步驟:
1. 爲 dclSingleton 分配內存空間
2. 初始化 dclSingleton
3. 將 dclSingleton 執行分配的內存地址
複製代碼

因爲jvm具備指令重排的特性,在多線程環境下就可能會出現一個線程獲取到的實例還未被初始化的狀況。例如:線程T1執行了1和3,此時線程T2調用 getDclSingleton() 方法後發現 dclSingleton 不爲空,所以會返回 dclSingleton,可是此時 dclSingleton 還未被初始化。所以在聲明靜態私有變量時添加volatile關鍵字保證jvm沒法進行指令重排,從而解決上述問題。併發

  1. 原來的同步方法變成了同步塊。
if (dclSingleton == null) {
    synchronized (DCLSingleton.class) {
        dclSingleton = new DCLSingleton();
    }
}
複製代碼

在只有一個 if 的代碼中,多線程條件下,假設線程T1和線程T2同時進入 dclSingleton == null 語句,接着T1或T2其中的一個線程會執行 dclSingleton = new DCLSingleton(); ,在執行結束以後會釋放鎖,另一個線程也會再次執行 dclSingleton = new DCLSingleton(); 語句,這致使構造函數執行了兩次,所以在同步代碼塊中,須要再次對 dclSingleton 是否爲空進行判斷。jvm

測試代碼

public class TestSingletons {
    public static void main(String[] args) {
        // 單線程
        DCLSingleton.getDclSingleton();
        DCLSingleton.getDclSingleton();
        DCLSingleton.getDclSingleton();
        DCLSingleton.getDclSingleton();

        // 多線程
        /*for (int i = 0; i < 10; i++) { new Thread(() -> { LazySafeSingleton.getSingleton(); }, String.valueOf(i)).start(); }*/
    }

}
複製代碼
  1. 單線程場景下:
  • 運行結果:
當前線程名稱: main	 我是構造方法...
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
singleton.DCLSingleton@4554617c
複製代碼
  • 結果分析: 構造方法只執行一次,單線程場景下是沒有問題的。
  1. 多線程場景下:
  • 運行結果:
當前線程名稱: 0	 我是構造方法...
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
singleton.LazySafeSingleton@2ceb2de
複製代碼
  • 結果分析: 屢次運行後的結果,會發現都只會構造一次。

餓漢式-線程安全

懶漢式與餓漢式的最主要區別在於,懶漢式的靜態私有變量爲空,在使用時進行構造;而餓漢式則在加載時就已經構造好了,即在使用前即已經構造完畢。這種方式會形成必定的資源浪費。

public class HungrySingleton {
    // 構造方法私有化
    private HungrySingleton() {
        System.out.println("當前線程名稱: " + Thread.currentThread().getName() + "\t 我是構造方法...");
    }

    // 靜態私有變量
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    // 提供靜態全局訪問點
    public static HungrySingleton getSingleton() {
        System.out.println(hungrySingleton); // 打印當前對象的惟一標識
        return hungrySingleton;
    }
}
複製代碼

測試方法與上面的懶漢式的建立方式一致,會發現不論是單線程環境下仍是多線程條件下,這種方式都是隻會構造一次。

其餘方式

靜態內部類

對 餓漢式-線程安全 的實現方式進行改進,能夠對 建立實例對象 和 使用實例對象 兩個步驟進行解耦,即實現使用時在進行建立。靜態內部類徹底符合。

public class InnerClazzSingleton {
    // 私有化構造方法
    private InnerClazzSingleton(){}

    // 靜態內部類,保證使用時才加載
    private static class InnerClassSingletonHolder {
        private static final InnerClazzSingleton SINGLETON = new InnerClazzSingleton();
    }

    // 提供靜態全局訪問點
    public static InnerClazzSingleton getInstance() {
        return InnerClassSingletonHolder.SINGLETON;
    }
}
複製代碼

這種方式利用了靜態內部類在使用時纔會進行加載的特性。即調用 getInstance() 方法時 InnerClassSingletonHolder 纔會被加載,此時會初始化 SINGLETON 實例,而且也能保證只被初始化一次。這種方式不只具備餓漢式的線程安全的特色,又具備延遲初始化節省系統資源的特色。 測試方法略。

延伸閱讀:朝花夕拾——Java靜態內部類加載

枚舉類

public enum EnumSingleton {
    INSTANCE;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
複製代碼

單例模式的應用

  • Logger Classes
  • Configuration Classes
  • Accesing resources in shared mode
  • Factories implemented as Singletons
  • java.lang.Runtime#getRuntime()
  • java.awt.Desktop#getDesktop()
  • java.lang.System#getSecurityManager()

總結

  1. 手寫單例模式的步驟
  • 在類中有一個自身的變量(這個變量能夠在使用時建立,也可使用前建立);
  • 確保全局只有該變量的一個實例;
  • 對外提供一個訪問該變量的公共方法;
  1. 各類模式的區別
不一樣的實現方式 特色
1. 懶漢式-線程不安全 線程不安全;具備延遲加載解決資源的特色;
2. 懶漢式-線程安全 對1進行改造——在全局訪問點處添加同步機制。能保證線程安全,雖然說多線程能保證一致性,可是沒法保證併發性
3. 雙重鎖校驗式-線程安全 對2進行改造,以求提升併發性,使用volatile 修飾靜態實例變量,同步前和同步後均須要校驗實例變量是否爲空。線程安全
4. 餓漢式-線程安全 對1進行改造,在使用前即會建立實例變量。全局只會建立一次,所以能保證線程安全,可是會形成資源浪費的問題
5. 靜態內部類-線程安全 對4進行改造,利用靜態內部類使用時纔會加載的特性將 實例變量的使用權 和 構造權 解耦。線程安全
6. 枚舉類-線程安全 線程安全,多適用於單元素場景

延伸閱讀: 單例模式

相關文章
相關標籤/搜索