重新講講單例模式和幾種實現
一、什麼講單例模式
單例模式,最簡單的理解是物件例項只有孤單的一份,不會重複建立例項。
這個模式已經很經典了,經典得我不再贅述理論,只給簡單註釋,畢竟教科書詳盡太多。
解決 sonar RSPEC-2168 異味的時候,發現目前業界推薦的單例模式和教科書上的已經有了較大差異,雙重鎖定不再推薦,甚至業內認為的最優方案不在sonar的推薦裡
於是提筆記錄,順帶補充了自己對多執行緒單例的理解 。
二、經典的單執行緒單例
這個部分沒有改動,簡單而經典,大致原始碼如下
public final class SignUtil { /** * 需要保持單例的物件 */ private static Object object; /** * 只允許SignUtil.getInstance獲取物件,也就是入口唯一 */ private SignUtil() { } /** * 物件的唯一出口 呼叫時才初始化(懶載入) * @return Object 確保單執行緒情況下這裡出去就是初始化好的 */ public static Object getInstance() { if (null == object) { object = new Object(); } return object; } /** * 內部函式也必須使用 getInstance這個入口 */ public static String getString() { return getInstance().toString(); } }
三、經典的雙重鎖定多執行緒單例 (JDK5-JDK7繼續適用)
public final class SignUtil { /** * 需要保持單例的物件 * 這裡需要宣告物件是易失的,因為object = new Object()不是一個原子操作,是被分拆為了例項化和初始化,一個申請空間,一個分配值 * 那麼就有可能出現 C在第三瞬間進入getInstance函式,發現null!=object,此時物件例項化了但沒初始化就直接返回,是個高危操作 */ private volatile static Object object; /** * 只允許SignUtil.getInstance獲取物件,也就是入口唯一 */ private SignUtil() { } /** * 物件的唯一出口 * * @return Object 多執行緒情況下這裡出去就是初始化好的 */ public static Object getInstance() { // 第0瞬間 A B 兩個執行緒同時初始化,一看都是null嘛 if (null == object) { // 第1瞬間 A B都進來了,因為不能重複初始化,所以被synchronized鎖約束開始競爭. // A 贏了SignUtil的物件鎖,B 只能等著 synchronized (SignUtil.class) { // 這裡為什麼不直接object = new Object()呢? // 因為B還等著呢,直接初始化就攔不住B再來一次初始化了. if (null == object) { // 第2瞬間, A終於初始化成功,且B不會重新初始化了. object = new Object(); // 第3瞬間,因為object被volatile約束了,可以視為原子操作,補上最後一個漏洞,成功返回。 } } } return object; } /** * 內部函式也必須使用 getInstance這個入口 */ public static String getString() { return getInstance().toString(); } }
四、 JDK8 以後的多執行緒單例
可以看到,三的要點太多了,很經典的雙重鎖定,但是不夠簡單優雅。目前更推薦下面兩種格式
4.1 JDK8 帶來的一個特性之一即是synchronized關鍵字,從原來的monitor重量級鎖,轉變成了由偏向鎖進行逐級升級到重量級鎖。換句話說,使用synchronized的代價被降低了,我們可以將上面的函式進行一個改進,讓它保持簡單和優雅。
但是代價依舊存在,以下適合併發衝突不嚴重的專案。
public final class SignUtil { /** * 需要保持單例的物件 */ private static Object object; /** * 只允許SignUtil.getInstance獲取物件,也就是入口唯一 */ private SignUtil() { } /** * 物件的唯一出口 是的,僅比單執行緒版多了一個synchronized * @return Object 由於synchronized,同一瞬間只能有一個物件進行獲取例項 */ public static synchronized Object getInstance() { if (null == object) { object = new Object(); } return object; } /** * 內部函式也必須使用 getInstance這個入口 */ public static String getString() { return getInstance().toString(); } }
4.2 利用靜態內部類的初始化特性
很巧妙地利用了jvm的類載入機制。那就是靜態內部類的延遲載入性完成單例。
public final class SignUtil {
/**
* 利用jvm的初始化規則 靜態內部類的靜態內部物件,只有在呼叫時才對靜態類開始初始化,
* 類的初始化過程是執行緒安全的,所以也只有一個執行緒能進行初始化
*/
private static class Node {
/**
* 在讀寫呼叫時才真正初始化,也就是懶載入
*/
private static final Object object = new Object();
}
/**
* 只允許SignUtil.getInstance獲取物件,也就是入口唯一
*/
private SignUtil() {
}
/**
* 不再是物件的唯一出口,其他地方也只要讀寫都能完成初始化
*
* @return Object 呼叫時,會觸發內部靜態類的初始化,返回時,初始化已完成
*/
public static Object getInstance() {
return Node.object;
}
/**
* 內部函式終於不用再依賴 getInstance這個入口
*/
public static String getString() {
return Node.object.toString();
}
}
五、 有沒有辦法讓單例模式不單例?
聽起來很魔鬼,但實際上,上述的多執行緒程單例都有兩個共同的缺陷可以做到:a 反射Constructor::setAccessible將私有建構函式改為公有函式 b.序列化時還是會返回多個例項。
解決方法為改造建構函式和申明readResolve函式,參考如下,解決方案是通用的。
public final class SignUtil {
private static volatile boolean init = false;
private static class Node {
private static final Object object = new Object();
}
/**
* 新增一個volatile的變數去判斷,防止反射初始化
* 第二次初始化會丟擲類強制轉換異常 當然你也可以用其他執行時異常
*/
private SignUtil() {
if (!init) {
init = true;
} else {
throw new ClassCastException();
}
}
public static Object getInstance() {
return Node.object;
}
public static String getString() {
return Node.object.toString();
}
/**
* 反序列化時直接返回單例的物件,這麼寫的原因在 ObjectInputStream::readUnshared裡
*/
private Object readResolve() {
return Node.object;
}
}
六、列舉單例
6.1 單元素列舉單例
和4.2一樣,《Effective Java 》找到了另一種利用jvm類載入機制實現單例的方法:單元素列舉單例。
這裡有幾個前提:
- Enum禁用了預設序列化。Enum::readObject、Enum::readObjectNoData約束了列舉物件的預設反序列化,保證序列化安全
- Enum提供了自己的序列化。Enum::toString 返回的是屬性名稱name,再通過Enum::valueOf把name轉回例項,保證了列舉不會被“退貨”(這個直譯了,大概是final且不會被clone的意思)。
- 這裡說一下valueOf的底層是Class::enumConstantDirectory,作用是呼叫時,生產一個Map<name, 列舉>的對映,而這個map很像單執行緒單例模式,但他不是靜態共享變數,所以是執行緒安全的,
不得不說,單元素列舉的確成功避免了重重的繁瑣,但代價是沒有了懶載入的特性,變成了餓漢模式
public enum SignUtil {
/**
* 從javap的反編譯結果看,會變成一個類公開的靜態變數,也就是餓漢模式
* public static final SignUtil INSTANCE = new SignUtil();
* 也就是會在載入類時直接初始化INSTANCE物件,而object物件是在構造時作為內部變數初始化,而建構函式是由jvm保證的
*/
INSTANCE;
/**
* 由於INSTANCE單例,所以object才是單例的
*/
private final Object object = new Object();
public Object getInstance() {
return object;
}
public String getString() {
return object.toString();
}
}
補一下javap反編譯後的結果
public final class SignUtil extends java.lang.Enum<SignUtil> {
public static final SignUtil INSTANCE;
private final java.lang.Object object;
private static final SignUtil[] $VALUES;
public static SignUtil[] values();
public static SignUtil valueOf(java.lang.String);
private SignUtil(java.lang.Object);
public java.lang.Object getInstance();
public java.lang.String getString();
static {};
}
6.2 多元素列舉的單例呢?
由於多元素列舉的建構函式可以被反射修改成公用函式並設定object,但由於INSTANCE和object都是final約束的,所以修改就會報錯,以此保證了單例性。
所以按照理解 多元素列舉也能完成單例,只是適用場景偏少
public enum SignUtil {
/*
* 對的,唯一的區別就是由無參變成了有參構造,本質是不變的餓漢
* public static final SignUtil INSTANCE = new SignUtil(new Object());
*/
INSTANCE(new Object()),
OTHER(new Object());
private final Object object;
private SignUtil(Object object) {
this.object = object;
}
public Object getInstance() {
return this.object;
}
public String getString() {
return this.object.toString();
}
}