1. 程式人生 > >3. 【建立與銷燬物件】用同步、靜態內部類和列舉型別強化單例模式

3. 【建立與銷燬物件】用同步、靜態內部類和列舉型別強化單例模式

本文是《Effective Java》讀書筆記第3條。

單例模式,顧名思義,就是當你需要並且僅需要某個類只有一個例項的時候所採用的設計模式。

/**
 * 餓漢式單例模式
 */
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    // 靜態工廠方法
    public Singleton getInstance() {
        return instance;
    }
}

如上,這就是一個單例模式,單例模式要保證達到如下基本目標:
1. 單例類只能有一個例項(static保證了這一點);
2. 單例類必須自己建立自己的唯一例項(private的構造方法);
3. 單例類必須給所有其他物件提供這一例項(例中只能由getInstance()方法得到單例物件)。

但是需要提醒一點:享有特權的客戶端可以藉助AccessibleObject.setAccessible方法,通過反射機制呼叫私有構造器。如果需要抵禦這種攻擊,可以修改構造器,讓它在被要求建立第二個例項的時候丟擲異常。

延遲建立物件

剛才的例子通常被叫做“餓漢式”單例模式,所謂“餓漢式”是相對於“懶漢式”來說的,“餓漢式”是在類載入之初就建立例項了的,而“懶漢式”是在使用的時候延遲建立例項物件的,這種方式在建立物件開銷比較大時推薦使用:

/**
 * 懶漢式單例模式
 */
public class Singleton {

    private static Singleton instance;
    private
Singleton() {} // 靜態工廠方法 public Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

同步

有經驗的開發人員馬上就會發現,這個程式碼不是執行緒安全的,併發環境下很可能出現多個Singleton例項。
解決方法1:最簡單的方式就是給getInstance()方法增加synchronized關鍵字。

    public
synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }

但是每次呼叫getInstance()都要同步,而實際使用過程中除了建立物件的時候絕大部分情況下是不需要同步的,稍微改動一下,以便在已經存在單例物件後就不要同步了:

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

靜態內部類

雖然效能提高了些,但是程式碼貌似不太雅緻啊,通常開發人員都有潔癖的,怎麼受得了?試試靜態內部類吧:

public class Singleton {

    private Singleton() {}

    private static class SingletonHolder {
        private final static Singleton INSTANCE = new Singleton();
    }

    public Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

因為載入Singleton類的時候,並不一定初始化靜態內部類SingletonHolder,當呼叫getInstance()方法時才會載入SingletonHolder類,同時建立單例物件,所以其實是在懶漢式單例模式裡邊嵌套了餓漢式單例模式。這樣既保證了執行緒安全,同時避免了同步帶來的效能開銷。

對於“餓漢式”和“懶漢式”的單例模式來說,建立過程分別在類載入時和例項物件使用時,如果類比較複雜,則需要注意根據不同的場景選擇使用。

列舉型別

多執行緒的問題解決了,但是還存在其他問題,那就是當Singleton類在需要序列化的時候,為了維護並保證是單例的,必須宣告所有的例項域都是transient的,並提供一個readResolve()方法,否則每次反序列化一個例項時都會建立一個新的例項。

    private Object readResolve() {
        return INSTANCE;
    }

具體原理在後續的讀書筆記中會詳細展開。
為了簡化這個問題,在Java 1.5之後,就可以採用只包含單個元素的列舉型別的方式來實現單例。

public enum Singleton4 {
    INSTANCE;

    public void doSth() {}
}

這種方式最簡潔,藉助列舉的序列化機制,即使是在面對複雜的序列化或反射攻擊的時候,也能夠絕對防止多次例項化,幾乎已經是實現Singleton的最佳方法了。

總結

通常來說,普通的應用環境下,“餓漢式”單例模式就可以滿足要求,簡單有效;當特別考慮需要採用延遲建立物件的場景的時候,建議採用靜態內部類的單例模式;最後,單元素列舉型別雖然使用不多,但是在需要序列化的時候可以作為最佳單例模式的實現思路。