💛原文地址為https://www.cnblogs.com/haixiang/p/12063951.html,轉載請注明出處!
簡介
單例模式主要是為了避免因為創建了多個實例造成資源的浪費,且多個實例由於多次調用容易導致結果出現錯誤,而使用單例模式能夠保證整個應用中有且只有一個實例。
要求
只需要三部分即可保證只有一個實例
- 不允許被外部類 new 對象
- 在本類中創建對象
- 對外提供接口調用創建好的對象
實現思路
針對上述三個條件我們用三種方式來保證這種要求
- 構造方法私有化
- 在本類中 new 一個本類對象
- 創建一個 public 方法返回上步創建好的對象以供外部調用
懶漢式單例模式
這種方式在類創建的時候就實例化對象,不存在多線程問題,缺點是提前創建對象,若未被使用會造成資源浪費。
public class SingletonIns {
private static SingletonIns singletonIns = new SingletonIns();
private SingletonIns() {}
public static SingletonIns getInstance() {
return singletonIns;
}
}
餓漢式單例模式
餓漢式單例優缺點與懶漢式正好相反,如果不作處理會有並發的問題,但是按需實例化,能避免資源被浪費的情況出現。
以下是最佳實踐的例子
public class SingletonIns {
private static volatile SingletonIns singletonIns = null;
private SingletonIns() {}
public static SingletonIns getInstance() {
if (null == singletonIns) {
synchronized (SingletonIns.class) {
if (null == singletonIns) {
singletonIns = new SingletonIns();
}
}
}
return singletonIns;
}
}
以下這種方式效率較低,在jdk以前的版本中synchronized
性能極差,后續有了自旋鎖、偏量鎖等優化才慢慢改善,現在仍然不建議這樣使用單例模式
public class SingletonIns {
private static SingletonIns singletonIns = null;
private SingletonIns() {}
public synchronized static SingletonIns getInstance() {
if (null == singletonIns) {
singletonIns = new SingletonIns();
}
return singletonIns;
}
}
以下這種方式存在線程安全的問題,例如a線程進入if判斷,b線程已經進入2處,會導致實例化兩個不同的對象
public class SingletonIns {
private static SingletonIns singletonIns = null;
private SingletonIns() {}
public static SingletonIns getInstance() {
if (null == singletonIns) { //1 有線程安全問題
synchronized(SingletonIns.class) {
singletonIns = new SingletonIns();//2
}
}
return singletonIns;
}
}
以下這種方式使用雙重校驗來避免上一種線程安全問題的出現,但是仍然存在線程安全問題。這是因為在線程執行到第1處的時候,代碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化
public class SingletonIns {
private static SingletonIns instance = null;
private SingletonIns() {}
public SingletonIns getInstance() {
if (null == instance) { //1
synchronized(SingletonIns.class) {
if(null == instance)
instance = new SingletonIns();//2
}
}
return instance;
}
}
為什么會出現這樣的問題呢?主要的原因是重排序。重排序是指編譯器和處理器為了優化程序性能而對指令序列進行重新排序的一種手段。
第2處的代碼創建了一個對象,這一行代碼可以分解成3個操作:
Copymemory = allocate(); // 1:分配對象的內存空間
ctorInstance(memory); // 2:初始化對象
instance = memory; // 3:設置instance指向剛分配的內存地址
根源在於代碼中的2和3之間,可能會被重排序。例如:
Copymemory = allocate(); // 1:分配對象的內存空間
instance = memory; // 3:設置instance指向剛分配的內存地址
// 注意,此時對象還沒有被初始化!
ctorInstance(memory); // 2:初始化對象java
這在單線程環境下是沒有問題的,但在多線程環境下會出現問題:B線程會看到一個還沒有被初始化的對象。
A2和A3的重排序不影響線程A的最終結果,但會導致線程B在B1處判斷出instance不為空,線程B接下來將訪問instance引用的對象。此時,線程B將會訪問到一個還未初始化的對象。
重排序能夠保證單線程的情況下,執行結果與按順序執行結果一致,但是無法保證多線程下結果正確。所以我們需要使用volatile
來修飾instance
,來禁用重排序,保證線程安全