Java設計模式-單例模式-餓漢-懶漢-餓漢線程不安全解決

目錄

  1. 爲什麼需要單例
  2. 餓漢模式的簡單實現
  3. 懶漢模式的簡單實現
  4. 二者比較
  5. 解決懶漢模式的線程不安全問題

爲什麼需要單例

單例模式能夠保證一個類僅有唯一的實例,並提供一個全局訪問點。

我們是不是可以通過一個全局變量來實現單例模式的要求呢?我們只要仔細地想想看,全局變量確實可以提供一個全局訪問點,但是它不能防止別人實例化多個對象。通過外部程序來控制的對象的產生的個數,勢必會系統的增加管理成本,增大模塊之間的耦合度。所以,最好的解決辦法就是讓類自己負責保存它的唯一實例,並且讓這個類保證不會產生第二個實例,同時提供一個讓外部對象訪問該實例的方法。自己的事情自己辦,而不是由別人代辦,這非常符合面向對象的封裝原則。

單例模式主要有3個特點:

  1. 單例類確保自己只有一個實例。
  2. 單例類必須自己創建自己的實例。
  3. 單例類必須爲其他對象提供唯一的實例。

餓漢模式

實現單例的餓漢模式主要有3步:

  1. 讓默認的構造函數私有化,使外部類無法通過new的方式獲取該類的實例
private SingletonHunger() {}
  1. 我們依舊要提供實例給外部,而外部類又無法取得該類的實例對象,所以我們將實例以靜態變量的方式提供
public static SingletonHunger instance = new SingletonHunger();
public static void main(String[] args) {
    SingletonHunger s1 = SingletonHunger.instance;
    SingletonHunger s2 = SingletonHunger.instance;
    System.out.println(s1 == s2); // true
}
  1. 將instance類變量私有化並提供getter訪問器
private static SingletonHunger instance = new SingletonHunger();

public static SingletonHunger getInstance() {
    return instance;
}

餓漢模式有什麼特點呢,可以看到最明顯的就是instance是靜態成員變量,它在類被加載的時候就會被實例化,不管有沒有被其它外部類訪問,所以這種一開始就實例化好的,我們覺得它很着急,所以叫餓漢模式。

public class SingletonHunger {

    // 1. 將默認的構造函數私有化
    private SingletonHunger() {}

    // 2. 提供靜態變量
    private static SingletonHunger instance = new SingletonHunger();

    // 3. 創建instance變量的getter訪問器
    public static SingletonHunger getInstance() {
        return instance;
    }
}

懶漢模式

懶漢模式和餓漢模式的卻別就在於懶漢模式只是聲明類的實例變量,而在有外部類(線程)訪問的時候纔去真正實例化(開闢內存空間),之後的所有線程都共享最先創建的那個實例

public class SingletonLazy {

    // 1. 將默認的構造函數私有化
    private SingletonLazy() {
    }

    // 2. 聲明類的唯一實例 只是聲明
    private static SingletonLazy instance;

    // 3. 爲instance提供訪問器
    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

二者比較

餓漢模式:類加載時較慢,訪問對象時較快,線程安全

懶漢模式:類加載時較快,訪問對象時較慢,線程不安全

大多數應用場景下都使用懶漢模式,因爲餓漢模式還有一個問題那便是內存空間的浪費。所以我們就需要解決懶漢模式的線程不安全問題

解決懶漢模式的線程不安全問題

主要解決的問題是兩個

  1. 線程的併發
  2. JVM的指令重排

第一個問題我們可以用加鎖來實現,用sychronizd即可,第二個問題是Java的關鍵字volatile。

**加鎖:**我們可以直接加在getter上,但是這樣子的靜態方法鎖整個臨界區比較大,比較耗費資源,所以使用同步代碼塊

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

爲什麼要判空兩次?其實就是爲了用同步代碼塊,你必須保證臨界區完成一整套必不可少的操作,最開始的判空只是判斷是否需要進入臨界區。加入兩個線程同時停在了第一個判空處,其中一個線程獲得鎖進去不判空直接new,那麼它完成操作釋放鎖之後對於第二個等待鎖的線程而言,它獲得一釋放的鎖之後也是進去直接new,很顯然,這一點都不符合臨界區的設計。

volatile

instance = new Singleton();

這條語句並不是一個原子操作

  1. 分配內存給對象,在內存中開闢一段地址空間;// Singleton var = new Singleton();
  2. 對象的初始化;// var = init();
  3. 將分配好對象的地址指向instance變量,即instance變量的寫;// instance = var;
    設想一下,如果不使用volatile關鍵字限制指令的重排序,1-3-2操作,獲得到單例的線程可能拿到了一個空對象,後續操作會有影響!因此需要引入volatile對變量進行修飾。

再詳細的內容參考博客

所以最後我們得到的結果爲這樣

public class SingletonLazy {

    // 1. 將默認的構造函數私有化
    private SingletonLazy() {
    }

    // 2. 聲明類的唯一實例 只是聲明
    private volatile static SingletonLazy instance;

    // 3. 爲instance提供訪問器

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

參考博客

java單例設計模式詳解(懶漢餓漢式)+深入分析爲什麼懶漢式是線程不安全的+解決辦法:https://blog.csdn.net/yaoyaoyao_123/article/details/84799861

【JAVA】線程安全的懶漢模式爲什麼要使用volatile關鍵字:https://blog.csdn.net/weixin_42078452/article/details/84892372

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