一、什麼講單例模式 單例模式,最簡單的理解是對象實例只有孤單的一份,不會重覆創建實例。 這個模式已經很經典了,經典得我不再贅述理論,只給簡單註釋,畢竟教科書詳盡太多。 解決 sonar RSPEC-2168 異味的時候,發現目前業界推薦的單例模式和教科書上的已經有了較大差異,雙重鎖定不再推薦,甚至業 ...
目錄
一、什麼講單例模式
單例模式,最簡單的理解是對象實例只有孤單的一份,不會重覆創建實例。
這個模式已經很經典了,經典得我不再贅述理論,只給簡單註釋,畢竟教科書詳盡太多。
解決 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();
}
}