單例模式(Singleton)

Sidney發表於2014-12-21

單例模式

意圖

  • 單例模式用來保證一個類只有一個例項, 這個例項不是外界手動 new 出來的, 而是對外提供一個方法來訪問到它.

  • 單例模式封裝了初始化方法, 保證這個例項只被初始化一次.

要解決什麼問題

有時候程式需要一個唯一的物件, 它在整個程式的宣告週期中只存在一個例項. 比如:

  • Logger. 很多情況下. 我們希望所有物件都往一個檔案中寫 log. 在建立 Logger 的時候, 假如我們每次都用:

    Logger logger = new Logger(“dir/log.file”);
    

那麼我們有可能會出現日誌檔案會被重新清空(開啟檔案時候選擇, 清空開啟), 甚至使得日誌檔案內容錯亂(假如多個執行緒同時呼叫 log 方法).

  • Factory. 很多工廠只用初始化一次就夠了.
  • Service. 有的 Service 可能初始化比較複雜, 但是功能知識相當於一個 Delegate, 這種情況下, Service 做成可以做成單例.

討論

基本思路

通常情況下, 想要獲得一個類的一個例項, 無非就是 new 出來. 只要有對建構函式的訪問許可權, 我們想要多少個例項就能 new 多少個例項. 那麼, 我們怎麼才能做到讓一個類只能有一個例項呢?

答案就是: 控制訪問許可權.

因為, 不管 public 也好 protected 也好, package 也好, 只要有一定的可見性, 建立的權利就完全交給外界了. 只有設定為 private, 限制了構造方法可見性, 也就控制了外界建立物件的權利.

可是, 將構造方法設定為 private, 外界的確無法建立了, 那麼這個類還有什麼用? 例項都建立不了呀!

其實, 雖然外界建立不了這個例項, 但是類本身還是可以訪問自己的建構函式從而建立例項出來. 我們用一個 field 儲存一個例項, 然後我們對外提供一個 public 的方法, 以便外界通過這個方法訪問這個例項. 現在, 我們來想象一下這個類的樣子:

public class A {
    private A instance = new A();
    private A() {
    }
    public A getInstance() {
        return instance;
    }
}

這裡有個問題, 這個 getInstance() 方法該怎麼呼叫? 這個方法必須要有例項才能呼叫, 可是我們這個方法就是用來建立例項的. 該怎麼辦? 有沒有一種方法能不讓我們建立例項就可以呼叫類的方法的?

靜態方法! 只需要這個類被載入且被初始化就能通過 類名.靜態方法名() 就可以呼叫到. 好! 現在我們來修改一下啊這個類:

public class A {
    // 因為靜態方法只能訪問靜態變數, 所以要把 instance 設為靜態以供 getInstance() 訪問
    private static A instance = new A();
    private A() {
    }
    public static A getInstance() {
        return instance;
    }
}

延時載入

現在, 讓我們捋一捋這個例項是如何建立的. 我們有一個 A 型別的field. 這個filed會在類完成裝載,連結後在初始化階段(靜態初始化器 static initializer)被賦值. 之後, 不管有沒有呼叫類的 getInstance() 方法,都會存在於應用程式中. 這裡又有問題了, 不管用沒用到 getInstance() 方法, 這個 field 所代表的例項一定會被建立! 那初始化過程中虛擬機器任務是多麼繁重? 我們能不能改成用到的時候才初始化? 答案是可以的, 我們可以將這個例項的建立延時成第一次呼叫 getInstance() 時:

public class A {
    // 1. 因為靜態方法只能訪問靜態變數, 所以要把 instance 設為靜態以供 getInstance() 訪問
    // 2. 設為 null
    private static A instance = null
    private A() {
    }
    public static A getInstance() {
        // 第一次呼叫 getInstance() 時, instance 一定為 null, 這時候要進行例項的建立
        if (instance == null) {
            instance = new A();
        }
        // instance 除了第一次之外, 都不為null, 可以直接返回.
        return instance;
    }
}

多執行緒問題

之前, 我們使用

private static A instance = new A();

這種方式的時候, 我們是不需要考慮多執行緒情況的, 因為 VM 會處理的很好. 但是現在放在了 getInstance() 裡, 就需要我們自己控制多執行緒併發問題了. 試想一下, 假如有多個執行緒同時呼叫 getInstance() 方法會怎樣? 當程式碼執行到

    if (instance == null) {
        instance = new A();
    }

時候, 第一個執行緒判斷 instancenull, 然後打算執行 instance = new A(), 但是沒有真的執行, instance 仍為 null; 然而在這時, 時間片用光了, 假設輪到第二個執行緒執行, 同樣判斷 instancenull, 執行了 instance = new A(), 這時第二個時間片用完了, 有可能切回第一個執行緒, 這時, 第一個執行緒繼續執行 instance = new A(). 問題就來了, getInstance() 為這兩個執行緒返回了兩個不同的例項! 這不是我們想要的!

好在對於執行緒安全問題,我們早已經有很成熟的解決方法, 就是關鍵字 synchronized. 我們再修改一下程式碼吧:

public class A {
    // 1. 因為靜態方法只能訪問靜態變數, 所以要把 instance 設為靜態以供 getInstance() 訪問
    // 2. 設為 null
    private static A instance = null
    private A() {
    }
    // 加上synchronized 關鍵字解決執行緒安全問題
    public synchronized static A getInstance() {
        // 第一次呼叫 getInstance() 時, instance 一定為 null, 這時候要進行例項的建立
        if (instance == null) {
            instance = new A();
        }
        // instance 除了第一次之外, 都不為null, 可以直接返回.
        return instance;
}
}

這樣的單例模式已經滿足大部分需求了. 但是假如有多個執行緒且呼叫 getInstance() 特別頻繁, 那麼加在方法上的 synchronized 關鍵字可能會降低程式的執行效率. 所以執行緒安全的程式碼還可以再優化一下:

public class A {
    // 1. 因為靜態方法只能訪問靜態變數, 所以要把 instance 設為靜態以供 getInstance() 訪問
    // 2. 設為 null
    // 3. 使用 volatile 關鍵字, 保證多個執行緒能正確處理對 instance 的例項化
    private volatile* static A instance = null
    private A() {
    }
    public static A getInstance() {
        // 第一次呼叫 getInstance() 時, instance 一定為 null, 這時候要進行例項的建立
        if (instance == null) {
            synchronized (A.class) {
                // 還得判斷問題, 防止不在同一個時間片中執行
                if (instance == null) { 
                    instance = new A();
                }        
            }
        }
        // instance 除了第一次之外, 都不為null, 可以直接返回.
        return instance;
    }
}

這樣的單例模式的實現, 即照顧到了多執行緒, 又照顧到了效率.

要點

  1. 有一個 private static 修飾的屬性
  2. 建構函式要被定義為 private
  3. 有一個 public static 修飾的方法的訪問方法
  4. 訪問方法中要延時載入
  5. 多執行緒安全問題要用 synchronized
  6. 外界適用時要通過訪問方法來後去類的例項

聲音

Q: 通過不同的引數來初始化單例模式是個好主意麼? 比如:

`public static A getInstance(Config config)`

A: 不是, 根據單例的定義, 一個物件只能被初始化一次, 如果getInstance() 能被傳入引數,那麼外界可以用不同引數來獲取不同的例項了. 這就不再是單例了. 用工廠模式可能會是個更好的選擇.

詳情請見: http://stackoverflow.com/questions/1050991/singleton-with-arguments-in-java

Q: 單例真的好麼? A: 篇幅太長, 意見也不統一, 但是有一些觀點還是值得我們借鑑.

http://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons

相關文章