1. 程式人生 > >雙重檢查鎖實現單例(java)

雙重檢查鎖實現單例(java)

urn rdl == ini var import 如何 安全 why

單例類在Java開發者中非常常用,但是它給初級開發者們造成了很多挑戰。他們所面對的其中一個關鍵挑戰是,怎樣確保單例類的行為是單例?也就是說,無論任何原因,如何防止單例類有多個實例。在整個應用生命周期中,要保證只有一個單例類的實例被創建,雙重檢查鎖(Double checked locking of Singleton)是一種實現方法。顧名思義,在雙重檢查鎖中,代碼會檢查兩次單例類是否有已存在的實例,一次加鎖一次不加鎖,一次確保不會有多個實例被創建。順便提一下,在JDK1.5中,Java修復了其內存模型的問題。在JDK1.5之前,這種方法會有問題。本文中,我們將會看到怎樣用Java實現雙重檢查鎖的單例類,為什麽Java 5之前的版本雙重檢查鎖會有問題,以及怎麽解決這個問題。順便說一下,這也是重要的面試要點,我曾經在金融業和服務業的公司面試被要求手寫雙重檢查鎖實現單例模式、相信我,這很棘手,除非你清楚理解了你在做什麽。你也可以閱讀我的完整列表“單例模式設計問題”來更好的準備面試。

為什麽你需要雙重檢查鎖來實現單例類?

一個常見情景,單例類在多線程環境中違反契約。如果你要一個新手寫出單例模式,可能會得到下面的代碼:

技術分享圖片
1 private static Singleton _instance;
2  
3 public static Singleton getInstance() {
4     if (_instance == null) {
5         _instance = new Singleton();
6     }
7     return _instance;
8 }
技術分享圖片

然後,當你指出這段代碼在超過一個線程並行被調用的時候會創建多個實例的問題時,他很可能會把整個getInstance()

方法設為同步(synchronized),就像我們展示的第二段示例代碼getInstanceTS()方法一樣。盡管這樣做到了線程安全,並且解決了多實例問題,但並不高效。在任何調用這個方法的時候,你都需要承受同步帶來的性能開銷,然而同步只在第一次調用的時候才被需要,也就是單例類實例創建的時候。這將促使我們使用雙重檢查鎖模式(double checked locking pattern),一種只在臨界區代碼加鎖的方法。程序員稱其為雙重檢查鎖,因為會有兩次檢查 _instance == null,一次不加鎖,另一次在同步塊上加鎖。這就是使用Java雙重檢查鎖的示例:

技術分享圖片
 1 public static Singleton getInstanceDC() {
 2     if (_instance == null) {                // Single Checked
 3         synchronized (Singleton.class) {
 4             if (_instance == null) {        // Double checked
 5                 _instance = new Singleton();
 6             }
 7         }
 8     }
 9     return _instance;
10 }
技術分享圖片

這個方法表面上看起來很完美,你只需要付出一次同步塊的開銷,但它依然有問題。除非你聲明_instance變量時使用了volatile關鍵字。沒有volatile修飾符,可能出現Java中的另一個線程看到個初始化了一半的_instance的情況,但使用了volatile變量後,就能保證先行發生關系(happens-before relationship)。對於volatile變量_instance,所有的寫(write)都將先行發生於讀(read),在Java 5之前不是這樣,所以在這之前使用雙重檢查鎖有問題。現在,有了先行發生的保障(happens-before guarantee),你可以安全地假設其會工作良好。另外,這不是創建線程安全的單例模式的最好方法,你可以使用枚舉實現單例模式,這種方法在實例創建時提供了內置的線程安全。另一種方法是使用靜態持有者模式(static holder pattern)。

技術分享圖片
 1 /*
 2  * A journey to write double checked locking of Singleton class in Java.
 3  */
 4  
 5 class Singleton {
 6  
 7     private volatile static Singleton _instance;
 8  
 9     private Singleton() {
10         // preventing Singleton object instantiation from outside
11     }
12  
13     /*
14      * 1st version: creates multiple instance if two thread access
15      * this method simultaneously
16      */
17  
18     public static Singleton getInstance() {
19         if (_instance == null) {
20             _instance = new Singleton();
21         }
22         return _instance;
23     }
24  
25    /*
26     * 2nd version : this definitely thread-safe and only
27     * creates one instance of Singleton on concurrent environment
28     * but unnecessarily expensive due to cost of synchronization
29     * at every call.
30     */
31  
32    public static synchronized Singleton getInstanceTS() {
33        if (_instance == null) {
34            _instance = new Singleton();
35        }
36        return _instance;
37    }
38  
39    /*
40     * 3rd version : An implementation of double checked locking of Singleton.
41     * Intention is to minimize cost of synchronization and  improve performance,
42     * by only locking critical section of code, the code which creates instance of Singleton class.
43     * By the way this is still broken, if we don‘t make _instance volatile, as another thread can
44     * see a half initialized instance of Singleton.
45     */
46  
47     public static Singleton getInstanceDC() {
48         if (_instance == null) {
49             synchronized (Singleton.class) {
50                 if (_instance == null) {
51                     _instance = new Singleton();
52                 }
53             }
54         }
55         return _instance;
56     }
57 }
技術分享圖片

這就是本文的所有內容了。這是個用Java創建線程安全單例模式的有爭議的方法,使用枚舉實現單例類更簡單有效。我並不建議你像這樣實現單例模式,因為用Java有許多更好的方式。但是,這個問題有歷史意義,也教授了並發是如何引入一些微妙錯誤的。正如之前所說,這是面試中非常重要的一點。在去參加任何Java面試之前,要練習手寫雙重檢查鎖實現單例類。這將增強你發現Java程序員們所犯編碼錯誤的洞察力。另外,在現在的測試驅動開發中,單例模式由於難以被模擬其行為而被視為反模式(anti pattern),所以如果你是測試驅動開發的開發者,最好避免使用單例模式。

雙重檢查鎖實現單例(java)