昨天讀到了公眾號“Import New”的Hi,我們再來聊一聊 Java 的單例 很有收獲,在此做個簡單的記錄。
1.懶漢式
1.1簡單式
// Version 1
public class Single1 {
private static Single1 instance;
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
// Version 1.1
public class Single1 {
private static Single1 instance;
private Single1() {}
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
問題:多個線程同時訪問,如果有多個線程同時運行到if (instance == null)時,都判斷為null,這個時候就不是單例了。
想法:加上synchronized同步鎖
1.2synchronized版本
// Version 2
public class Single2 {
private static Single2 instance;
private Single2() {}
public static synchronized Single2 getInstance() {
if (instance == null) {
instance = new Single2();
}
return instance;
}
}
問題:給gitInstance方法加鎖,雖然會避免了可能會出現的多個實例問題,但是會強制除T1之外的所有線程等待,實際上會對程序的執行效率造成負面影響。
想法:double-check
1.3 double-chek版本
// Version 3
public class Single3 {
private static Single3 instance;
private Single3() {}
public static Single3 getInstance() {
if (instance == null) {
synchronized (Single3.class) {
if (instance == null) {
instance = new Single3();
}
}
}
return instance;
}
}
第一個if (instance == null)是為了解決上一方案中的效率問題
第二個if (instance == null)是為了防止多個實例
問題:1、instance = new Single3();非原子操作 2、會受到指令重排的影響。
原子操作:簡單來說,原子操作(atomic)就是不可分割的操作,在計算機中,就是指不會因為線程調度被打斷的操作。
例如:賦值操作
m = 6;
指令重排:簡單來說,就是計算機為了提高執行效率,會做的一些優化,在不影響最終結果的情況下,可能會對一些語句的執行順序進行調整。
下面這段話直接從陳皓的文章(深入淺出單實例SINGLETON設計模式)中復制而來:
主要在于singleton = new Singleton()這句,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
給 singleton 分配內存
調用 Singleton 的構造函數來初始化成員變量,形成實例
將singleton對象指向分配的內存空間(執行完這步 singleton才是非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執行完畢、2 未執行之前,被線程二搶占了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。
再稍微解釋一下,就是說,由于有一個『instance已經不為null但是仍沒有完成初始化』的中間狀態,而這個時候,如果有其他線程剛好運行到第一層if (instance == null)這里,這里讀取到的instance已經不為null了,所以就直接把這個中間狀態的instance拿去用了,就會產生問題。
這里的關鍵在于——線程T1對instance的寫操作沒有完成,線程T2就執行了讀操作。
1.4 終極版本:volatile
// Version 4
public class Single4 {
private static volatile Single4 instance;
private Single4() {}
public static Single4 getInstance() {
if (instance == null) {
synchronized (Single4.class) {
if (instance == null) {
instance = new Single4();
}
}
}
return instance;
}
}
volatile關鍵字的一個作用是禁止指令重排,把instance聲明為volatile之后,對它的寫操作就會有一個內存屏障(什么是內存屏障?),這樣,在它的賦值完成之前,就不用會調用讀操作。
注意:volatile阻止的不是singleton = new Singleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操作([1-2-3])完成之前,不會調用讀操作(if (instance == null))。
2.餓漢式單例
餓漢式單例是指:指全局的單例實例在類裝載時構建的實現方式。
2.1 餓漢式單例的實現方式
//餓漢式實現
public class SingleB {
private static final SingleB INSTANCE = new SingleB();
private SingleB() {}
public static SingleB getInstance() {
return INSTANCE;
}
}
問題:INSTANCE的初始化是在類加載時進行的,而類的加載是由ClassLoader來做的,所以開發者本來對于它初始化的時機就很難去準確把握。
3 其他的一些方式
3.1 Effective Java 1 —— 靜態內部類
// Effective Java 第一版推薦寫法
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3.2 Effective Java 2 —— 枚舉
// Effective Java 第二版推薦寫法
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}
// 使用
SingleInstance.INSTANCE.fun1();