java單例模式雙重檢驗鎖的優缺點?還有哪些實現方式?列舉一些使用場景
2018年7月18日,在專案程式碼中看到單例模式,總結一下單例模式的知識點.
單例模式的概念:
在應用程式的生命週期中,在任意時刻,引用某個類的例項都是同一個.在一個系統中有些類只需要有一個全域性物件,統一管理系統行為和執行某些操作.例如在使用hibernate時,sessionFactory介面負責初始化hibernate,它充當資料儲存源的代理,並負責初始化session物件,通常一個專案只需要一個sessionFactory物件即可(多資料庫時每個資料庫對應一個sessionFactory),那麼就可把sessionFactory單例化,提高系統性能.
實現單例模式的思路:
思考1:如果我們通過new關鍵字建立某個類的物件,那麼new出來的物件在記憶體中佔有不同的地址,肯定不是單例.所以我們要保證單例,首先要保證不能通過new關鍵字來建立該類物件.
思考2:如何保證不能new出該類物件呢?顯然只需要私有化構造方法即可.
思考3:那我們需要的物件從哪來呢?只需要在該類內部建立一個該類物件,建立一個公共方法返回該物件即可,為了保證單例,用static關鍵字保證記憶體地址唯一.這樣該類物件引用始終指向同一個記憶體地址.
單例模式的實現方式:
懶漢式方式一:單執行緒下
-
public class Singleton {
-
private static Singleton instance = null;
-
//構造私有化,外界不能new物件
-
private Singleton (){}
-
//通過公共的方式對外提供一個例項
-
public static Singleton getInstance() {
-
if (instance == null) {
-
//如果沒有例項化,就建立一個物件
-
instance = new Singleton();
-
}
-
return instance;
-
}
-
}
優點:不呼叫getInstance()就不會例項化,提高效率.
缺點:在單個執行緒中沒有問題,但多個執行緒同時訪問的時候就可能同時建立多個例項,而且這多個例項不是同一個物件,雖然後面建立的例項會覆蓋先建立的例項,但是還是會存在拿到不同物件的情況.
懶漢式方式二:synchronized同步方法
-
public class Singleton{
-
private static final Singleton instance = null;
-
private Singleton(){}
-
//同步方法
-
public static synchronized Singleton getInstance(){
-
if(instance==null){
-
instance = new Singleton();
-
}
-
return instance;
-
}
-
}
雖然做到了執行緒安全,並且解決了多例項的問題,但是它並不高效.因為在任何時候只能有一個執行緒呼叫 getInstance()方法.但是同步操作只需要在第一次呼叫時才被需要,即第一次建立單例例項物件時.這就引出了雙重檢驗鎖.
懶漢式方式三:雙重檢驗鎖
-
public class Singleton {
-
private volatile static Singleton instance = null;
-
private Singleton (){}
-
public static Singleton getSingleton() {
-
if (instance == null) { //判斷是否為null
-
synchronized (Singleton.class) {
-
if (instance == null) { //判斷是否為null
-
instance = new Singleton();
-
}
-
}
-
}
-
return instance;
-
}
-
}
這段程式碼看起來很完美,但還是有問題,主要在於 instance = new Singleton()這句程式碼.因為這並非一個原子性操作,實際上在JVM裡大概做了3件事:
1.給instance分配記憶體
2.呼叫Singleton構造完成初始化
3.使instance物件的引用指向分配的記憶體空間(完成這一步instance就不是null了)
但是在 JVM 的即時編譯器中存在指令重排序的優化.也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2.如果是後者,則在 3 執行完畢、2 未執行之前,被執行緒二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以執行緒二會直接返回 instance,然後使用,然後順理成章地報錯.(為什麼使用了synchronized同步還會被其他執行緒搶佔?)
我們只需要將 instance 變數宣告成 volatile 就可以了
-
public class Singleton {
-
private volatile static Singleton instance; //宣告成 volatile
-
private Singleton (){}
-
public static Singleton getSingleton() {
-
if (instance == null) {
-
synchronized (Singleton.class) {
-
if (instance == null) {
-
instance = new Singleton();
-
}
-
}
-
}
-
return instance;
-
}
-
}
使用 volatile 的主要原因是其一個特性:禁止指令重排序優化。也就是說,在 volatile 變數的賦值操作後面會有一個記憶體屏障(生成的彙編程式碼上),讀操作不會被重排序到記憶體屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變數的寫操作都先行發生於後面對這個變數的讀操作(這裡的“後面”是時間上的先後順序)。
但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 記憶體模型)是存在缺陷的,即時將變數宣告成 volatile 也不能完全避免重排序,主要是 volatile 變數前後的程式碼仍然存在重排序問題。這個 volatile 遮蔽重排序的問題在 Java 5 中才得以修復,所以在這之後才可以放心使用 volatile。
相信你不會喜歡這種複雜又隱含問題的方式,當然我們有更好的實現執行緒安全的單例模式的辦法。
餓漢式:載入類時初始化例項
-
public class Singleton{
-
//類載入時就初始化
-
private static final Singleton instance = new Singleton();
-
private Singleton(){}
-
public static Singleton getInstance(){
-
return instance;
-
}
-
}
優點:這種方法非常簡單,因為單例的例項被宣告成 static 和 final 變量了,在第一次載入類到記憶體中時就會初始化,所以建立例項本身是執行緒安全的.
缺點: 資源利用率不高,可能getInstance()永遠不會執行到,但執行該類的其他靜態方法或者載入了該類(class.forName),那麼這個例項仍然初始化.餓漢式的建立方式在一些場景中將無法使用:譬如 Singleton 例項的建立是依賴引數或者配置檔案的,在 getInstance()之前必須呼叫某個方法設定引數給它,那樣這種單例寫法就無法使用了
懶漢式方式四:靜態內部類
-
public class Singleton {
-
private static class SingletonHolder {
-
private static final Singleton INSTANCE = new Singleton();
-
}
-
private Singleton (){}
-
public static final Singleton getInstance() {
-
return SingletonHolder.INSTANCE;
-
}
-
}
這種寫法仍然使用JVM本身機制保證了執行緒安全問題;由於 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;同時讀取例項的時候不會進行同步,沒有效能缺陷;也不依賴 JDK 版本。
列舉方式:Enum
用列舉寫單例實在太簡單了!這也是它最大的優點。下面這段程式碼就是宣告列舉例項的通常做法。
-
public enum EasySingleton{
-
INSTANCE;
-
//變數
-
//方法
-
}
我們可以通過EasySingleton.INSTANCE來訪問例項,這比呼叫getInstance()方法簡單多了。建立列舉預設就是執行緒安全的,所以不需要擔心double checked locking,而且還能防止反序列化導致重新建立新的物件。在《Effective Java》中說列舉是實現單例的最佳方式.建議在實際專案中單例以列舉方式實現.
總結:
一般來說,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、列舉。
一般情況下直接使用餓漢式就好了,如果明確要求要懶載入(lazy initialization)會傾向於使用靜態內部類,如果涉及到反序列化建立物件時會試著使用列舉的方式來實現單例。
文章參考自: