設計模式(一)—— 單例模式
Java中單例模式是一種應用非常廣泛的設計模式,它主要用來保證java的某個類只有一個例項存在, 可以避免例項物件的重複建立,從而節約時間、空間,並且可以避免由於操作多個例項帶來的邏輯錯誤。如果一個物件的使用貫穿整個應用程式,而且起到了全域性統一管控的作用,那麼單例模式也許是一種不錯的選擇。
單例模式雖然有多種寫法,但大部分寫法都有不足, 下面逐一介紹。
1. 餓漢模式
public class Singleton{ private Singleton(){} private static Singleton singleton = new Singleton(); public static Singleton getInstance() { return singleton; } }
首先看到建構函式必須是private的,保證其他類不能直接例項化此類,而是通過靜態方法返回一個靜態例項;
餓漢模式是在類載入的時候就對例項進行建立,例項在整個程式週期都存在。它的好處是隻有類載入的時候建立一次例項,不會存在多個執行緒建立多個例項的情況,避免了多執行緒同步問題。 但是它的問題也很明顯,即使這個例項沒有用到也會被建立,而且在類載入後就建立到了,記憶體就被浪費了。
這種實現方式適合單例佔用記憶體比較小, 在初始化時就會被用到的情況。但是, 如果單例佔用記憶體比較大,或單例只是在某個特定場景下用到,使用餓漢模式就不合適了,這時候就需要用到懶漢模式進行延遲載入。
2. 懶漢模式
public class Singleton {
private Singleton(){}
private static Singleton singleton;
publice static getInstance() {
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
懶漢模式中的例項是在用到的時候才去建立,如果單例使用次數少,並建立例項消耗資源較多,那麼可以選擇懶漢模式;但是懶漢模式沒有考慮執行緒安全問題,在多執行緒環境下, 可能會建立多個例項,因此需要加鎖來解決執行緒同步問題。如下:
public class Singleton {
private Singleton() {}
private static Singleton singleton;
private static synchronized Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
加鎖後的懶漢模式解決了執行緒安全問題,但是它存在著效能問題,synchronized修飾的同步方法比一般方法要慢很多,下一個執行緒想要獲取例項必須要等上一個執行緒釋放鎖後才可以,如果呼叫次數較多,就造成了較大的效能損耗。它也不夠完美,不推薦用。因此有了雙重校驗鎖的實現方式。
3. 雙重校驗鎖
public class Singleton{
private Singleton() {}
private static Singleton singleton;
public static Singleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
if(singleton == null) { //2
singleton = new Singleton();
}
}
}
return singleton;
}
}
可以看到上面的同步程式碼塊外多了一層判空操作,由於單例物件只需要建立一次,如果後面再次呼叫getInstance()只需要直接返回例項物件。因此大部分情況下,都不會執行到同步程式碼塊,從而提高了效能。
不過還需要考慮一種情況,假如兩個執行緒A、B,A執行了if(singleton == null)語句, 它認為單例沒有建立,此時B也執行到了同樣語句, B也認為單例物件沒有建立,然後兩個執行緒依次執行同步程式碼塊,並分別建立了一個單例物件。為了解決這個問題,我們還需要在同步程式碼塊中增加一次判空操作,如上面程式碼塊中的//2;
我們看到雙重校驗鎖既實現了延遲載入,又解決了執行緒併發的問題,同時還解決了執行效率問題,是否就真的萬無一失了呢?
這裡要提到Java中的指令重排優化,所謂指令重排優化就是指在不改變原語意的情況下, 通過調整指令的執行順序讓程式執行的更快。JVM中沒有規定編譯器優化相關的內容,也就是說JVM可以自由的進行指令重排優化。
這個問題的關鍵就在指令重排的存在,會導致初始化Singleton和將物件地址賦給singleton欄位的順序不確定。在某個執行緒建立單例物件時, 在構造方法被呼叫之前,就為該物件分配了了記憶體空間並將物件的欄位設定為預設值。此時就可能將分配的記憶體地址賦值給了singleton欄位了,然而該物件還沒有初始化。若緊接著另一個執行緒來呼叫getInstance(), 通過singleton欄位取到的例項就不是正確的Singleton例項。
以上就是雙重校驗鎖會失效的原因,不過還好在JDK1.5及以後版本增加了volatile關鍵字。volatile的一個語義就是禁止指令重排優化,也就保證了instance變數被賦值的時候物件已經是初始化過的,從而避免了上面說到的問題。增加了volatile關鍵字的程式碼如下:
public class Singleton{
private Singleton() {}
private static volatile Singleton singleton;
public static Singleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
if(singleton == null) { //2
singleton = new Singleton();
}
}
}
return singleton;
}
}
以上就是完善的雙重校驗鎖單例模式,也是比較推薦使用的一種。
4.靜態內部類
public class Singleton{
private Singleton() {}
private static class SingletonHolder {
public static Singleton singleton = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
這種方式利用類載入機制來保證只建立一個例項,與餓漢模式一樣,也是利用類載入機制,因此不存在多執行緒併發問題。
不一樣的是,它是在內部類裡面去建立例項,這樣的話,只要應用中不使用內部類,JVM就不會去載入這個類,也就不會建立單例物件,從而實現延遲載入。同時它的實現方式也比較簡單,推薦使用。
5. 列舉
public enum Singleton {
singleton;
public void whateverMethod() {}
}
上面提到的四種實現單例方式都有共同的缺點:
(1)需要額外的工作來實現序列化,否則每次反序列化一個序列化的物件都會建立一個新的例項。
(2)可以使用反射強制呼叫私有構造器(如果要避免這種情況,可以修改構造器,讓它在建立第二個例項的時候報錯)。
而列舉類很好地解決了這兩個問題,使用列舉除了執行緒安全和防止反射呼叫構造器之外,還提供了自動序列化機制,防止反序列化的時候建立新的物件。但是實際工作中,很少見有人使用列舉單例。