【深入】java 單例模式

轉自:http://www.cnblogs.com/coffee/archive/2011/12/05/inside-java-singleton.html

關於單例模式的文章,其實網上早就已經氾濫了。但一個小小的單例,裏面卻是有着許多的變化。網上的文章大多也是提到了其中的一個或幾個點,很少有比較全面且脈絡清晰的文章,於是,我便萌生了寫這篇文章的念頭。企圖把這個單例說透,說深入。但願我不會做的太差。

  首先來看一個典型的實現:

複製代碼

 1 /**
 2 *
基礎的單例模式,Lazy模式,非線程安全
 3  * 優點:lazy,初次使用時實例化單例,避免資源浪費
 4  * 缺點:1、lazy,如果實例初始化非常耗時,初始使用時,可能造成性能問題
 5  * 2、非線程安全。多線程下可能會有多個實例被初始化。
 6  *
 7 * @author laichendong
 8 * @since 2011-12-5
 9  */
10 public class SingletonOne {
11    
12     /**
單例實例變量 */
13     private staticSingletonOne instance = null;
14    
15     /**
16      *
私有化的構造方法,保證外部的類不能通過構造器來實例化。
17*/
18     private SingletonOne() {
19        
20     }
21    
22     /**
23      *
獲取單例對象實例
24      *
25      * @return
單例對象
26*/
27     public staticSingletonOne getInstance() {
28        if (instance == null){ // 1
29             instance = new SingletonOne(); // 2
30         }
31        return instance;
32     }
33    
34 }

複製代碼

  註釋中已經有簡單的分析了。接下來分析一下關於“非線程安全”的部分。

  1、當線程A進入到第28行(#1)時,檢查instance是否爲空,此時是空的。
  2、此時,線程B也進入到28行(#1)。切換到線程B執行。同樣檢查instance爲空,於是往下執行29行(#2),創建了一個實例。接着返回了。
  3、在切換回線程A,由於之前檢查到instance爲空。所以也會執行29行(#2)創建實例。返回。
  4、至此,已經有兩個實例被創建了,這不是我們所希望的。 

 怎麼解決線程安全問題?

  方法一:同步方法。即在getInstance()方法上加上synchronized關鍵字。這時單例變成了  

http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif

複製代碼

 1 /**
 2 * copyright sf-express Inc
 3  */
 4 package com.something.singleton;
 5
 6 /**
 7 *
同步方法 的單例模式,Lazy模式,線程安全
 8  * 優點:
 9  * 1、lazy,初次使用時實例化單例,避免資源浪費
10  * 2、線程安全
11  * 缺點:
12  * 1、lazy,如果實例初始化非常耗時,初始使用時,可能造成性能問題
13  * 2、每次調用getInstance()都要獲得同步鎖,性能消耗。
14  *
15  * @author laichendong
16  * @since 2011-12-5
17 */
18 public class SingletonTwo {
19    
20     /**
單例實例變量 */
21     private staticSingletonTwo instance = null;
22    
23     /**
24      *
私有化的構造方法,保證外部的類不能通過構造器來實例化。
25 */
26     private SingletonTwo() {
27        
28     }
29    
30     /**
31      *
獲取單例對象實例
32      * 同步方法,實現線程互斥訪問,保證線程安全。
33      *
34      * @return
單例對象
35 */
36     public static synchronized SingletonTwo getInstance() {
37        if (instance == null){ // 1
38             instance = new SingletonTwo(); // 2
39         }
40        return instance;
41     }
42    
43 }

複製代碼

 

  加上synchronized後確實實現了線程的互斥訪問getInstance()方法。從而保證了線程安全。但是這樣就完美了麼?我們看。其實在典型實現裏,會導致問題的只是當instance還沒有被實例化的時候,多個線程訪問#1的代碼纔會導致問題。而當instance已經實例化完成後。每次調用getInstance(),其實都是直接返回的。即使是多個線程訪問,也不會出問題。但給方法加上synchronized後。所有getInstance()的調用都要同步了。其實我們只是在第一次調用的時候要同步。而同步需要消耗性能。這就是問題。

  方法二:雙重檢查加鎖Double-checked locking。
  
其實經過分析發現,我們只要保證 instance = new SingletonOne(); 是線程互斥訪問的就可以保證線程安全了。那把同步方法加以改造,只用synchronized塊包裹這一句。就得到了下面的代碼:

複製代碼

1     public static SingletonThree getInstance() {
2        if (instance == null){ // 1
3             synchronized(SingletonThree.class) {
4                 instance = new SingletonThree(); // 2
5             }
6        }
7        return instance;
8     }

複製代碼

 

  這個方法可行麼?分析一下發現是不行的!
  1、線程A和線程B同時進入//1的位置。這時instance是爲空的。
  2、線程A進入synchronized塊,創建實例,線程B等待。
  3、線程A返回,線程B繼續進入synchronized塊,創建實例。。。
  4、這時已經有兩個實例創建了。 

  爲了解決這個問題。我們需要在//2的之前,再加上一次檢查instance是否被實例化。(雙重檢查加鎖)接下來,代碼變成了這樣:

複製代碼

 1     public staticSingletonThree getInstance() {
 2         if(instance == null) { //1
 3             synchronized(SingletonThree.class) {
 4                 if(instance == null) {
 5                     instance = new SingletonThree(); // 2
 6                 }
 7             }
 8         }
 9         returninstance;
10     }

複製代碼

 

  這樣,當線程A返回,線程B進入synchronized塊後,會先檢查一下instance實例是否被創建,這時實例已經被線程A創建過了。所以線程B不會再創建實例,而是直接返回。貌似!到 此爲止,這個問題已經被我們完美的解決了。遺憾的是,事實完全不是這樣!這個方法在單核和多核的cpu下都不能保證很好的工作。導致這個方法失敗的原因是當前java平臺的內存模型。java平臺內存模型中有一個叫“無序寫”(out-of- order writes)的機制。正是這個機制導致了雙重檢查加鎖方法的失效。這個問題的關鍵在上面代碼上的第5行:instance= new SingletonThree(); 這行其實做了兩個事情:1、調用構造方法,創建了一個實例。2、把這個實例賦值給instance這個實例變量。可問題就是,這兩步jvm是不保證順序的。也就是說。可能在調用構造方法之前,instance已經被設置爲非空了。下面我們看一下出問題的過程:
  1、線程A進入getInstance()方法。
  2、因爲此時instance爲空,所以線程A進入synchronized塊。
  3、線程A執行 instance= new SingletonThree(); 把實例變量instance設置成了非空。(注意,實在調用構造方法之前。)
  4、線程A退出,線程B進入。
  5、線程B檢查instance是否爲空,此時不爲空(第三步的時候被線程A設置成了非空)。線程B返回instance的引用。(問題出現了,這時instance的引用並不是SingletonThree的實例,因爲沒有調用構造方法。) 
  6、線程B退出,線程A進入。
  7、線程A繼續調用構造方法,完成instance的初始化,再返回。 

  好吧,繼續努力,解決由“無序寫”帶來的問題。

複製代碼

 1     public staticSingletonThree getInstance() {
 2         if(instance == null) {
 3             synchronized(SingletonThree.class) {           // 1
 4                 SingletonThree temp = instance;             // 2
 5                 if(temp == null) {
 6                     synchronized(SingletonThree.class) {   // 3
 7                         temp = new SingletonThree();        // 4
 8                     }
 9                     instance = temp;                        // 5
10                 }
11            }
12        }
13        return instance;
14     }

複製代碼

 

  解釋一下執行步驟。
  1、線程A進入getInstance()方法。
  2、因爲instance是空的 ,所以線程A進入位置//1的第一個synchronized塊。
  3、線程A執行位置//2的代碼,把instance賦值給本地變量temp。instance爲空,所以temp也爲空。 
  4、因爲temp爲空,所以線程A進入位置//3的第二個synchronized塊。
  5、線程A執行位置//4的代碼,把temp設置成非空,但還沒有調用構造方法!(“無序寫”問題) 
  6、線程A阻塞,線程B進入getInstance()方法。
  7、因爲instance爲空,所以線程B試圖進入第一個synchronized塊。但由於線程A已經在裏面了。所以無法進入。線程B阻塞。
  8、線程A激活,繼續執行位置//4的代碼。調用構造方法。生成實例。
  9、將temp的實例引用賦值給instance。退出兩個synchronized塊。返回實例。
  10、線程B激活,進入第一個synchronized塊。
  11、線程B執行位置//2的代碼,把instance實例賦值給temp本地變量。
  12、線程B判斷本地變量temp不爲空,所以跳過if塊。返回instance實例。

  好吧,問題終於解決了,線程安全了。但是我們的代碼由最初的3行代碼變成了現在的一大坨~。於是又有了下面的方法。

  方法三:預先初始化static變量。

複製代碼

 1 /**
 2 *
預先初始化static變量 的單例模式  非Lazy  線程安全
 3  * 優點:
 4  * 1、線程安全
 5  * 缺點:
 6  * 1、非懶加載,如果構造的單例很大,構造完又遲遲不使用,會導致資源浪費。
 7  *
 8 * @author laichendong
 9 * @since 2011-12-5
10 */
11 public class SingletonFour {
12    
13     /**
單例變量 ,static的,在類加載時進行初始化一次,保證線程安全 */
14     private staticSingletonFour instance = new SingletonFour();
15    
16     /**
17      *
私有化的構造方法,保證外部的類不能通過構造器來實例化。
18      */
19     private SingletonFour() {
20        
21     }
22    
23     /**
24      *
獲取單例對象實例
25      *
26      * @return
單例對象
27      */
28     public staticSingletonFour getInstance() {
29        return instance;
30     }
31    
32 }

複製代碼

  看到這個方法,世界又變得清淨了。由於java的機制,static的成員變量只在類加載的時候初始化一次,且類加載是線程安全的。所以這個方法實現的單例是線程安全的。但是這個方法卻犧牲了Lazy的特性。單例類加載的時候就實例化了。如註釋所述:非懶加載,如果構造的單例很大,構造完又遲遲不使用,會導致資源浪費。

  那到底有沒有完美的辦法?懶加載,線程安全,代碼簡單。

  方法四:使用內部類。

 

複製代碼

 1 /**
 2 *
基於內部類的單例模式  Lazy  線程安全
 3  * 優點:
 4  * 1、線程安全
 5  * 2、lazy
 6  * 缺點:
 7  * 1、待發現
 8  *
 9 * @author laichendong
10  * @since 2011-12-5
11 */
12 public class SingletonFive {
13    
14     /**
15      *
內部類,用於實現lzay機制
16 */
17     private static class SingletonHolder{
18        /**
單例變量  */
19        private staticSingletonFive instance = new SingletonFive();
20     }
21    
22     /**
23      *
私有化的構造方法,保證外部的類不能通過構造器來實例化。
24 */
25     private SingletonFive() {
26        
27     }
28    
29     /**
30      *
獲取單例對象實例
31      *
32      * @return
單例對象
33 */
34     public staticSingletonFive getInstance() {
35        return SingletonHolder.instance;
36     }
37    
38 }

複製代碼

  解釋一下,因爲java機制規定,內部類SingletonHolder只有在getInstance()方法第一次調用的時候纔會被加載(實現了lazy),而且其加載過程是線程安全的(實現線程安全)。內部類加載的時候實例化一次instance。

 

  最後,總結一下:
  1、如果單例對象不大,允許非懶加載,可以使用方法三。
  2、如果需要懶加載,且允許一部分性能損耗,可以使用方法一。(官方說目前高版本的synchronized已經比較快了)
  3、如果需要懶加載,且不怕麻煩,可以使用方法二。
  4、如果需要懶加載,沒有且!推薦使用方法四。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章