1. 程式人生 > >設計模式(一)—— 單例模式

設計模式(一)—— 單例模式

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)可以使用反射強制呼叫私有構造器(如果要避免這種情況,可以修改構造器,讓它在建立第二個例項的時候報錯)。

而列舉類很好地解決了這兩個問題,使用列舉除了執行緒安全和防止反射呼叫構造器之外,還提供了自動序列化機制,防止反序列化的時候建立新的物件。但是實際工作中,很少見有人使用列舉單例。

為什麼要用列舉實現單例模式(避免反射、序列化問題)