面試官:你簡歷上有熟悉設計模式,那你給我說一下單例模式實現及線程安全吧

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!


前言

單例應用的太廣泛,大家應該都用過,本文主要是想聊聊線程安全的單例以及反序列化破壞單例的情況。

1、概念

確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。

關鍵點:

  • 私有化構造函數
  • 通過一個靜態方法或枚舉返回單例類對象
  • 確保單例類的對象有且只有一個,尤其是多線程環境下
  • 確保單例類對象在反序列化時不會重新構建對象

2、實現

2.1、線程安全的單例

2.1.2、餓漢模式

餓漢模式:不管有沒有調用getInstance方法,只要類加載了,我就給你new出來(a)

public class A {

    private static final A a = new A();

    public static A getInstance() {
        return a;
    }

    private A() {}
}

以下兩點保證了以上代碼的線程安全:

  • 調用一個類的靜態方法的時候會觸發類的加載(如果類沒加載過)
  • 類只會加載(被加載到虛擬機內存的過程,包括5個階段)一次
  • static變量在類初始化的時候(類加載過程的最後一個階段)會去賦值靜態變量

2.1.2、懶漢模式

懶漢模式:延遲加載,用到再去new

public class B {
    private static volatile B b;

    public static synchronized B getInstance() {
        if (b == null) {
            b = new B();
        }
        return b;
    }

    private B() { }
}

要保證線程安全,最簡單的方式是加同步鎖。synchroized保證了多個線程串行的去調用getInstance(),既然是串行,那就不會存在什麼線程安全問題了。但是這實現,每次讀都要加鎖,其實我們想要做的只是讓他寫(new)的時候加鎖。

2.1.3、Double Check Lock (DCL)

public class B {
    private static volatile B b;

    public static synchronized B getInstance0() {
        if (b == null) {
            synchronized (B.class) {
                b = new B();
            }
        }
        return b;
    }

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

    private B() { }
}

爲了解決懶漢模式的效率問題,我們改造成getInstance0():

但還有個問題 X、Y 兩個線程同時進入if (b == null), X先進同步代碼塊,new了一個B,返回。Y等到X釋放鎖之後,它也進了同步代碼塊,也會new一個B。

getInstance0()解決了效率問題,但它不是線程安全的。我們有進行了一次改造: getInstance():

getInstance在同步塊裏面,又做了一次if (b == null)的判斷,確保了Y線程不會再new B,保證了線程安全。

getInstance() 也正是所謂的雙重檢查鎖定(double checked locking)。

這裏還有一個關鍵點:private static volatile B b; b是用volatile修飾的。

這個主要是因爲new 並不是原子的。

B b = new B();

可以簡單的分解成一下步驟:

  • 分配對象內存
  • 初始化對象
  • 設置引用指向分配的內存地址

2,3 直接可能發生指令重排序,就是說對象還未初始化完成,就讓b指向了一塊內存地址,這時候b就不是null了。

2.1.4、靜態內部類單例模式

public class C {
    private C() {}

    public static C getInstance() {
        return CHolder.c;
    }

    private static class CHolder {
        private static final C c = new C();
    }
}

靜態內部類的線程安全也是由jvm保證的,在調用Cholder.c的時候,去加載CHolder類,new 了一個c。

總的來說,這個方式比DCL還是高點的,因爲DCL加了volatile,效率上還是略微有些些影響。

上面介紹的3種線程安全的單例,在有種極端的情況,單例模式有可能被破壞:反序列化

Java序列化就是指把Java對象轉換爲字節序列的過程
Java反序列化就是指把字節序列恢復爲Java對象的過程。

反序列化的時候,會重新構造一個對象,破壞單例模式。我們看下代碼驗證下:

public class C1 implements Serializable {
    private C1() {
        System.out.println("構造方法");
    }

    public static C1 getInstance() {
        return CHolder.c;
    }

    private static class CHolder {
        private static final C1 c = new C1();
    }
// 注意這塊被註釋的代碼
//    private Object readResolve(){
//        System.out.println("read resolve");
//        return CHolder.c;
//    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        C1 c = C1.getInstance();
        System.out.println(c.toString());
        try {
            ObjectOutputStream o = new ObjectOutputStream(
                    new FileOutputStream("d:/tmp/c.out"));
            o.writeObject(c);
            o.close();
        } catch(Exception e) {
            e.printStackTrace();
        }

        C1 c1 = null, c2 = null;

        try {
            ObjectInputStream in =new ObjectInputStream(
                    new FileInputStream("d:/tmp/c.out"));
            c1 = (C1)in.readObject();
            in.close();
        } catch(Exception e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream in =new ObjectInputStream(
                    new FileInputStream("d:/tmp/c.out"));
            c2 = (C1)in.readObject();
            in.close();
        } catch(Exception e) {
            e.printStackTrace();
        }

        System.out.println("c1.equals(c2) : " + c1.equals(c2));
        System.out.println("c1 == c2 : " + (c1 == c2));
        System.out.println(c1);
        System.out.println(c2);
    }
}

結果:

構造方法
[email protected]
c1.equals(c2) : false
c1 == c2 : false
[email protected]
[email protected]

放開註釋的代碼

構造方法
[email protected]
read resolve
read resolve
c1.equals(c2) : true
c1 == c2 : true
[email protected]
[email protected]

正如我們看到的那樣,加上readResolve就解決了反序列化單例被破壞的問題。

當然,如果沒實現Serializable接口,也就不會有這個被破壞的問題… 還是看場景。

關於readResolve的介紹,感興趣的同學們可以看java.io.ObjectInputStream#readUnshared方法上的註釋(博主看了,看得不是很明白,一知半解,就不誤人子弟了)

而我們下面要介紹的枚舉單例,並不會有這個問題。

2.1.5、枚舉單例

public enum  DEnum {

    INSTANCE;

    private D d;

    DEnum() {
        d = new D();
    }

    public D getInstance() {
        return d;
    }
}
public class D {}

線程安全的保證:

  • 枚舉只能擁有私有的構造器
  • 枚舉類實際上是一個繼承Enum的一個final類
  • 上面的INSTANCE實際是被static final 修飾的

序列化不破壞單例的保證:

在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定製的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

2.2 線程不安全的單例

2.2.1、懶漢模式

不過多介紹了,這個其實在線程安全的單例部分,我們介紹的比較詳細了。

public class B {
    private static volatile B b;

    public static B getInstance() {
        if (b == null) {
            b = new B();
        }
        return b;
    }

    private B() { }
}

3. 總結

單例的應用實在是太多了,也沒必要再去找源碼種的經典使用(因爲基本上大家用過)。

枚舉單例構造方法還是public,並不是防止外部直接去new它。個人認爲如果一個類要開放給外部使用,用內部類的形式實現單例是最合適的。

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/live

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-08-04
本文作者:程序員偉傑
本文來自:“掘金”,瞭解相關信息可以關注“掘金”

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