天天看點

單例模式以及其中的線程安全問題1. 餓漢式2. 懶漢式3. 如何解決懶漢式中存在的線程安全問題?4. new對象的操作中的指令重排序問題5. 推薦使用餓漢式

有一些類,在記憶體中沒有必要存在多個對象。這時候就出現了單例模式。

1. 餓漢式

使用

static

保證了線程安全,在類加載到記憶體的時候,進行執行個體化。

/**
 * 餓漢式
 * 類加載到記憶體後,就執行個體化一個單例,JVM保證線程安全
 * 簡單實用,推薦使用!
 * 唯一缺點:不管用到與否,類裝載時就完成執行個體化
 * Class.forName("")
 * (話說你不用的,你裝載它幹啥)
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01(); 

    private Mgr01() {};

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2);
    }
}
           

2. 懶漢式

有人說上面的餓漢式,我都還沒有用,你就給我建立了一個對象,能不能在我用的時候再建立對象?于是又有了懶漢式。在調用

getInstance

方法的時候,才去建立對象,而且建立之前先判斷是不是為空。

單線程環境下,這段代碼确實沒有問題。但是多線程情況下,就會有問題。看代碼中的注釋(很好了解的)。

/**
 * lazy loading
 * 也稱懶漢式
 * 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
 */
public class Mgr03 {
    private static Mgr03 INSTANCE;

    private Mgr03() {
    }

    public static Mgr03 getInstance() {
        if (INSTANCE == null) { // 一個線程過來了,判斷了,INSTANCE 是null。這時候又有一個線程過來了,
        // 也判斷了INSTANCE 是null。然後第一個線程繼續執行,建立了一個對象。接着第二個線程繼續開始執行,
        //也會建立一個新的對象(第二個線程已經執行過判斷INSTANCE 是不是null的操作)。這時候就不能保證單例了。
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->
                System.out.println(Mgr03.getInstance().hashCode())
            ).start();
        }
    }
}
           

3. 如何解決懶漢式中存在的線程安全問題?

  1. getInstance

    方法上加個鎖不就行了

    确實能夠達到目的,但是又有人說了,整個函數加鎖,有效率問題。能不能将鎖細化?于是又有了第

    2

    種方案。
/**
 * lazy loading
 * 也稱懶漢式
 * 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
 * 可以通過synchronized解決,但也帶來效率下降
 */
public class Mgr04 {
    private static Mgr04 INSTANCE;

    private Mgr04() {
    }

    public static synchronized Mgr04 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr04.getInstance().hashCode());
            }).start();
        }
    }
}
           
  1. 加鎖之前,先判斷

    instance

    是不是

    null

    ,是

    null

    然後才加鎖。

乍一看,好像挺好的,沒啥問題。但是這種寫法也是有線程安全問題的(參考方法中的注釋)。

/**
 * lazy loading
 */
public class Mgr05 {
    private static Mgr05 INSTANCE;

    private Mgr05() {
    }

    public static Mgr05 getInstance() {
        if (INSTANCE == null) {  //  一個線程是來了,判斷INSTANCE 是null,這時候第二個線程來了,
        //也判斷了INSTANCE 是null。線程二獲得了鎖,然後建立了對象,執行完釋放了鎖;第二個線程獲得鎖,
        //繼續執行,也會建立一個新的對象(因為它沒有再次判斷INSTANCE 是不是null)。這不就有兩個對象了嗎?
            //妄圖通過減小同步代碼塊的方式提高效率,然後不可行
            synchronized (Mgr05.class) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr05();
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr05.getInstance().hashCode());
            }).start();
        }
    }
}
           
  1. 加鎖之後,再判斷

    INSTANCE

    是不是

    null

    上面的代碼是由于加鎖之後,沒有判斷

    INSTANCE

    是不是

    null

    導緻的。那簡單,加鎖之後再判斷一下

    INSTANCE

    是不是

    null

    不就解決了嗎?

    這就引出了單例模式

    Double Check Lock

    的寫法。也就是

    getInstance

    方法裡面,加鎖之前和之後分别檢查下

    INSTANCE

    是不是

    null

    沒問題了嗎?注意代碼中的

    volatile

    是注釋掉的。

    這就是一個面試題了。單例模式中的

    DCL

    寫法,執行個體變量是否需要加

    volatile

    以及為什麼?
/**
 * lazy loading
 */
public class Mgr06 {
    private static /*volatile*/ Mgr06 INSTANCE; //JIT

    private Mgr06() {
    }

    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //雙重檢查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}
           

4.

new

對象的操作中的指令重排序問題

要回答單例模式中的

DCL

寫法,執行個體變量是否需要加

volatile

以及為什麼?這個問題,首先得要清楚

new

一個對象的操作其實是分為三條指令的。

分别是申請記憶體賦預設值、初始化、将執行個體變量指向對象。

這三條指令是可能發生指令重排序的,初始化操作和将将執行個體變量指向對象的操作的順序會互換。這時候就會出現線程安全的問題。

第一個線程來了,判斷

INSTANCE

null

,然後加鎖,進入了

new

對象的過程,如果發生指令重排序(先對執行個體變量進行了指派操作),在

new

到一半的時候,

INSTANCE

已經被指派,這時候,第二個線程來了,判斷

INSTANCE

不是

null

,會直接傳回還沒有初始化完成的

INSTANCE

對象,就會出現問題。

volatile

禁止指令重排序。就可以解決上述問題。

單例模式以及其中的線程安全問題1. 餓漢式2. 懶漢式3. 如何解決懶漢式中存在的線程安全問題?4. new對象的操作中的指令重排序問題5. 推薦使用餓漢式

5. 推薦使用餓漢式

其實工作中使用餓漢式就夠了,沒必要搞得這麼複雜。所謂面試造火箭,工作擰螺絲。。。。内卷的厲害。

繼續閱讀