1. 程式人生 > >Java單例模式——並非看起來那麼簡單

Java單例模式——並非看起來那麼簡單

       Java中單例(Singleton)模式是一種廣泛使用的設計模式。單例模式的主要作用是保證在Java程式中,某個類只有一個例項存在。一些管理器和控制器常被設計成單例模式。

單例模式有很多好處,它能夠避免例項物件的重複建立,不僅可以減少每次建立物件的時間開銷,還可以節約記憶體空間;能夠避免由於操作多個例項導致的邏輯錯誤。如果一個物件有可能貫穿整個應用程式,而且起到了全域性統一管理控制的作用,那麼單例模式也許是一個值得考慮的選擇。

單例模式有很多種寫法,大部分寫法都或多或少有一些不足。下面將分別對這幾種寫法進行介紹。

1、餓漢模式

public class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton newInstance(){
        return instance;
    }
}
從程式碼中我們看到,類的建構函式定義為private的,保證其他類不能例項化此類,然後提供了一個靜態例項並返回給呼叫者。餓漢模式是最簡單的一種實現方式,餓漢模式在類載入的時候就對例項進行建立,例項在整個程式週期都存在。它的好處是隻在類載入的時候建立一次例項,不會存在多個執行緒建立多個例項的情況,避免了多執行緒同步的問題。它的缺點也很明顯,即使這個單例沒有用到也會被建立,而且在類載入之後就被建立,記憶體就被浪費了。

這種實現方式適合單例佔用記憶體比較小,在初始化時就會被用到的情況。但是,如果單例佔用的記憶體比較大,或單例只是在某個特定場景下才會用到,使用餓漢模式就不合適了,這時候就需要用到懶漢模式進行延遲載入。

2、懶漢模式

public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton newInstance(){
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

懶漢模式中單例是在需要的時候才去建立的,如果單例已經建立,再次呼叫獲取介面將不會重新建立新的物件,而是直接返回之前建立的物件。如果某個單例使用的次數少,並且建立單例消耗的資源較多,那麼就需要實現單例的按需建立,這個時候使用懶漢模式就是一個不錯的選擇。但是這裡的懶漢模式並沒有考慮執行緒安全問題,在多個執行緒可能會併發呼叫它的getInstance()方法,導致建立多個例項,因此需要加鎖解決執行緒同步問題,實現如下。

public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static synchronized Singleton newInstance(){
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

3、雙重校驗鎖

加鎖的懶漢模式看起來即解決了執行緒併發問題,又實現了延遲載入,然而它存在著效能問題,依然不夠完美。synchronized修飾的同步方法比一般方法要慢很多,如果多次呼叫getInstance(),累積的效能損耗就比較大了。因此就有了雙重校驗鎖,先看下它的實現程式碼。

public class Singleton {
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {//2
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
可以看到上面在同步程式碼塊外多了一層instance為空的判斷。由於單例物件只需要建立一次,如果後面再次呼叫getInstance()只需要直接返回單例物件。因此,大部分情況下,呼叫getInstance()都不會執行到同步程式碼塊,從而提高了程式效能。不過還需要考慮一種情況,假如兩個執行緒A、B,A執行了if (instance == null)語句,它會認為單例物件沒有建立,此時執行緒切到B也執行了同樣的語句,B也認為單例物件沒有建立,然後兩個執行緒依次執行同步程式碼塊,並分別建立了一個單例物件。為了解決這個問題,還需要在同步程式碼塊中增加if (instance == null)語句,也就是上面看到的程式碼2。

       我們看到雙重校驗鎖即實現了延遲載入,又解決了執行緒併發問題,同時還解決了執行效率問題,是否真的就萬無一失了呢?

這裡要提到Java中的指令重排優化。所謂指令重排優化是指在不改變原語義的情況下,通過調整指令的執行順序讓程式執行的更快。JVM中並沒有規定編譯器優化相關的內容,也就是說JVM可以自由的進行指令重排序的優化。

這個問題的關鍵就在於由於指令重排優化的存在,導致初始化Singleton和將物件地址賦給instance欄位的順序是不確定的。在某個執行緒建立單例物件時,在構造方法被呼叫之前,就為該物件分配了記憶體空間並將物件的欄位設定為預設值。此時就可以將分配的記憶體地址賦值給instance欄位了,然而該物件可能還沒有初始化。若緊接著另外一個執行緒來呼叫getInstance,取到的就是狀態不正確的物件,程式就會出錯。

以上就是雙重校驗鎖會失效的原因,不過還好在JDK1.5及之後版本增加了volatile關鍵字。volatile的一個語義是禁止指令重排序優化,也就保證了instance變數被賦值的時候物件已經是初始化過的,從而避免了上面說到的問題。程式碼如下:

public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

4、靜態內部類

除了上面的三種方式,還有另外一種實現單例的方式,通過靜態內部類來實現。首先看一下它的實現程式碼:

public class Singleton{
    private static class SingletonHolder{
        public static Singleton instance = new Singleton();
    }
    private Singleton(){}
    public static Singleton newInstance(){
        return SingletonHolder.instance;
    }
}

這種方式同樣利用了類載入機制來保證只建立一個instance例項。它與餓漢模式一樣,也是利用了類載入機制,因此不存在多執行緒併發的問題。不一樣的是,它是在內部類裡面去建立物件例項。這樣的話,只要應用中不使用內部類,JVM就不會去載入這個單例類,也就不會建立單例物件,從而實現懶漢式的延遲載入。也就是說這種方式可以同時保證延遲載入和執行緒安全。

5、列舉

再來看本文要介紹的最後一種實現方式:列舉。

public enum Singleton{
    instance;
    public void whateverMethod(){}    
}

上面提到的四種實現單例的方式都有共同的缺點:

1)需要額外的工作來實現序列化,否則每次反序列化一個序列化的物件時都會建立一個新的例項。

2)可以使用反射強行呼叫私有構造器(如果要避免這種情況,可以修改構造器,讓它在建立第二個例項的時候拋異常)。

而列舉類很好的解決了這兩個問題,使用列舉除了執行緒安全和防止反射呼叫構造器之外,還提供了自動序列化機制,防止反序列化的時候建立新的物件。因此,《Effective Java》作者推薦使用的方法。不過,在實際工作中,很少看見有人這麼寫。

總結

本文總結了五種Java中實現單例的方法,其中前兩種都不夠完美,雙重校驗鎖和靜態內部類的方式可以解決大部分問題,平時工作中使用的最多的也是這兩種方式。列舉方式雖然很完美的解決了各種問題,但是這種寫法多少讓人感覺有些生疏。個人的建議是,在沒有特殊需求的情況下,使用第三種和第四種方式實現單例模式。

參考文章:http://www.jfox.info/java-dan-li-mo-shi-de-ji-zhong-xie-fa

http://devbean.blog.51cto.com/448512/203501/