雲棲號資訊:【點擊查看更多行業資訊】
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!
前言
單例應用的太廣泛,大家應該都用過,本文主要是想聊聊線程安全的單例以及反序列化破壞單例的情況。
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);
}
}
結果:
構造方法
me.hhy.designpattern.singletonpattern.C1@1540e19d
c1.equals(c2) : false
c1 == c2 : false
me.hhy.designpattern.singletonpattern.C1@135fbaa4
me.hhy.designpattern.singletonpattern.C1@45ee12a7
放開註釋的代碼
構造方法
me.hhy.designpattern.singletonpattern.C1@1540e19d
read resolve
read resolve
c1.equals(c2) : true
c1 == c2 : true
me.hhy.designpattern.singletonpattern.C1@1540e19d
me.hhy.designpattern.singletonpattern.C1@1540e19d
正如我們看到的那樣,加上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
本文作者:程序員偉傑
本文來自:“掘金”,瞭解相關信息可以關注“掘金”