Java設計模式4:單例模式

五月的倉頡發表於2015-10-23

前言

非常重要,單例模式是各個Java專案中必不可少的一種設計模式。本文的關注點將重點放在單例模式的寫法以及每種寫法的執行緒安全性上。所謂"執行緒安全性"的意思就是保證在建立單例物件的時候不存在競爭,只會建立出一個單例物件。

 

單例模式

作為物件的建立模式,單例模式確保其某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項,這個類稱為單例類。單例模式有以下特點:

1、單例類只能有一個例項

2、單例類必須自己建立自己的唯一例項

3、單例類必須給其他所有物件提供這一例項

下面看一下單例模式的三種寫法,除了這三種寫法,靜態內部類的方式、靜態程式碼塊的方式、enum列舉的方式也都可以,不過異曲同工,這三種方式就不寫了。

 

餓漢式

顧名思義,餓漢式,就是使用類的時候不管用的是不是類中的單例部分,都直接建立出單例類,看一下餓漢式的寫法:

 1 public class EagerSingleton
 2 {
 3     private static EagerSingleton instance = new EagerSingleton();
 4     
 5     private EagerSingleton()
 6     {
 7         
 8     }
 9     
10     public static EagerSingleton getInstance()
11     {
12         return instance;
13     }
14 }

這就是餓漢式單例模式的寫法,也是一種比較常見的寫法。這種寫法會不會造成競爭,引發執行緒安全問題呢?答案是不會。可能有人會覺得奇怪:

第3行,CPU執行執行緒A,例項化一個EagerSingleton,沒有例項化完,CPU就從執行緒A切換到執行緒B了,執行緒B此時也例項化這個EagerSingleton,然後EagerSingleton被例項化出來了兩次,有兩份記憶體地址,不就有執行緒安全問題了嗎?

沒關係,我們完全不需要擔心這個問題,JDK已經幫我們想到了。Java虛擬機器2:Java記憶體區域及物件,文中可以看一下物件建立這一部分,沒有寫得很詳細,其實就是"虛擬機器採用了CAS配上失敗重試的方式保證更新更新操作的原子性和TLAB兩種方式來解決這個問題"。

 

懶漢式

同樣,顧名思義,這個人比較懶,只有當單例類用到的時候才會去建立這個單例類,看一下懶漢式的寫法:

 1 public class LazySingleton
 2 {
 3     private static LazySingleton instance = null;
 4     
 5     private LazySingleton()
 6     {
 7         
 8     }
 9     
10     public static LazySingleton getInstance()
11     {
12         if (instance == null)
13             instance = new LazySingleton();
14         return instance;
15     }
16 }

這種寫法基本不用,因為這是一種執行緒非安全的寫法。試想,執行緒A初次呼叫getInstance()方法,程式碼走到第12行,執行緒此時切換到執行緒B,執行緒B走到12行,看到instance是null,就new了一個LazySingleton出來,這時切換回執行緒A,執行緒A繼續走,也new了一個LazySingleton出來。這樣,單例類LazySingleton在記憶體中就有兩份引用了,這就違背了單例模式的本意了。

可能有人會想,CPU分的時間片再短也不至於getInstance()方法只執行一個判斷就切換執行緒了吧?問題是,萬一執行緒A呼叫LazySingleton.getInstance()之前已經執行過別的程式碼了呢,走到12行的時候剛好時間片到了,也是很正常的。

 

雙檢鎖

既然懶漢式是非執行緒安全的,那就要改進它。最直接的想法是,給getInstance方法加鎖不就好了,但是我們不需要給方法全部加鎖啊,只需要給方法的一部分加鎖就好了。基於這個考慮,引入了雙檢鎖(Double Check Lock,簡稱DCL)的寫法:

 1 public class DoubleCheckLockSingleton
 2 {
 3     private static DoubleCheckLockSingleton instance = null;
 4     
 5     private DoubleCheckLockSingleton()
 6     {
 7         
 8     }
 9     
10     public static DoubleCheckLockSingleton getInstance()
11     {
12         if (instance == null)
13         {
14             synchronized (DoubleCheckLockSingleton.class)
15             {
16                 if (instance == null)
17                     instance  = new DoubleCheckLockSingleton();
18             }
19         }
20         return instance;
21     }
22 }

雙檢鎖的寫法是不是執行緒安全的呢?是的,至於為什麼,不妨以分析懶漢式寫法的方式分析一下雙檢鎖的寫法。

執行緒A初次呼叫DoubleCheckLockSingleton.getInstance()方法,走12行,判斷instance為null,進入同步程式碼塊,此時執行緒切換到執行緒B,執行緒B呼叫DoubleCheckLockSingleton.getInstance()方法,由於同步程式碼塊外面的程式碼還是非同步執行的,所以執行緒B走12行,判斷instance為null,等待鎖。結果就是執行緒A例項化出了一個DoubleCheckLockSingleton,釋放鎖,執行緒B獲得鎖進入同步程式碼塊,判斷此時instance不為null了,並不例項化DoubleCheckLockSingleton。這樣,單例類就保證了在記憶體中只存在一份。

 

單例模式在Java中的應用及解讀

Runtime是一個典型的例子,看下JDK API對於這個類的解釋"每個Java應用程式都有一個Runtime類例項,使應用程式能夠與其執行的環境相連線,可以通過getRuntime方法獲取當前執行時。應用程式不能建立自己的Runtime類例項。",這段話,有兩點很重要:

1、每個應用程式都有一個Runtime類例項

2、應用程式不能建立自己的Runtime類例項

只有一個、不能自己建立,是不是典型的單例模式?看一下,Runtime類的寫法:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance 
     * methods and must be invoked with respect to the current runtime object. 
     * 
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() { 
    return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}

    ...
}

後面的就不黏貼了,到這裡已經足夠了,看到Runtime使用getRuntime()方法並讓構造方法私有保證程式中只有一個Runtime例項且Runtime例項不可以被使用者建立。

 

單例模式的好處

作為一種重要的設計模式,單例模式的好處有:

1、控制資源的使用,通過執行緒同步來控制資源的併發訪問

2、控制例項的產生,以達到節約資源的目的

3、控制資料的共享,在不建立直接關聯的條件下,讓多個不相關的程式或執行緒之間實現通訊

相關文章