1. 程式人生 > >java單例模式執行緒安全

java單例模式執行緒安全

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

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

複製程式碼
 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 static SingletonOne instance = null;
14
15 /**16 * 私有化的構造方法,保證外部的類不能通過構造器來例項化。
17*/
18 private SingletonOne() {
19
20 }
21
22 /**23 * 獲取單例物件例項
24 *
25 * @return 單例物件
26*/
27 public static SingletonOne getInstance() {
28 if (instance == null) { // 129 instance = new SingletonOne(); // 230 }
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關鍵字。這時單例變成了  

使用同步方法的單例

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

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

複製程式碼
1     public static SingletonThree getInstance() {
2 if (instance == null) { // 13 synchronized (SingletonThree.class) {
4 instance = new SingletonThree(); // 25 }
6 }
7 return instance;
8 }
複製程式碼

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

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

複製程式碼
 1     public static SingletonThree getInstance() {
2 if (instance == null) { // 1 3 synchronized (SingletonThree.class) {
4 if (instance == null) {
5 instance = new SingletonThree(); // 2 6 }
7 }
8 }
9 return instance;
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 static SingletonThree 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; // 510 }
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 static SingletonFour instance = new SingletonFour();
15
16 /**17 * 私有化的構造方法,保證外部的類不能通過構造器來例項化。
18 */
19 private SingletonFour() {
20
21 }
22
23 /**24 * 獲取單例物件例項
25 *
26 * @return 單例物件
27 */
28 public static SingletonFour 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 static SingletonFive instance = new SingletonFive();
20 }
21
22 /**23 * 私有化的構造方法,保證外部的類不能通過構造器來例項化。
24 */
25 private SingletonFive() {
26
27 }
28
29 /**30 * 獲取單例物件例項
31 *
32 * @return 單例物件
33 */
34 public static SingletonFive getInstance() {
35 return SingletonHolder.instance;
36 }
37
38 }
複製程式碼

  解釋一下,因為java機制規定,內部類SingletonHolder只有在getInstance()方法第一次呼叫的時候才會被載入(實現了lazy),而且其載入過程是執行緒安全的(實現執行緒安全)。內部類載入的時候例項化一次instance。

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