基於volatile解決雙重檢查鎖不安全問題

一、 問題分析

雙重檢查鎖(Double-checked Locking)可以降低直接使用synchronized同步共享資源帶來的性能開銷,使用DCL實現延遲加載的代碼如下:

 public class DoubleCheckedLocking {
	 private static Instance instance;
	 public static Instance getInstance() {
		if (null == instance) {
			synchronized(DoubleCheckedLocking.class) {
				if (null == instance)
					instance = new Instance();
			}
		}
		return instance;
    }
 }

DCL代碼先檢查共享變量是否爲null,若不爲null,則不需要執行加鎖和初始化操作,這樣就可以大幅降低synchronized 帶來的性能開銷。

但是這段代碼會存在訪問到仍爲初始化完成的對象,問題就在instance = new Instance();這句創建實例的代碼。這句代碼實際分爲3個步驟:

  1. 分配對象的內存空間
  2. 初始化對象
  3. 設置instance 變量指向分配的內存地址

根據Java SE 7 規範,所有線程在執行Java程序時必須遵循intra-thread semantics, 該規範保證重排序不會改變單線程的程序執行結果。
上述步驟中即使2、3之間的重排序,但在變單線程下,執行結果不會改變,即不會違反intra-thread semantics 規範。而且,這種重排序在沒有改變單線程執行結果的前提下,因爲並沒有一開始就初始化對象,可以提高程序的執行性能。

在多線程情況下,當A線程中2、3之間重排序後,A線程執行到第3步,還未初始化對象,此時B線程服務getInstance()方法,判斷instance不爲null返回對象,B線程將訪問到一個還未被初始化的對象。這就是雙重檢查鎖存在問題的根源

二、 解決方案

可以通過兩個思路來實現線程安全的延遲初始化:
1) 禁止步驟2、3重排序
2) 允許重排序,但其他線程看不到這個重排序過程

2.1 基於volatile的解決方案

對於思路1,很容易想到volatile關鍵字,當用volatile修飾變量後,在多線程環境下2、3 步驟會被禁止重排序,使用volatile的方案如下:

  public class DoubleCheckedLocking {
 	 private static volatile Instance instance; // 共享變量使用volatile關鍵字修飾
 	 public static Instance getInstance() {
 		if (null == instance) {
 			synchronized(DoubleCheckedLocking.class) {
 				if (null == instance)
 					instance = new Instance();
 			}
 		}
 		return instance;
    }
  }

注:基於volatile的解決方案需要JDK5 及以上的版本,因爲Java從JDK 5 開始使用新的JSR-133內存模型規範,增強了volatile語義:嚴格限制編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的獲取具有相同的內存語義,此時volatile相當於輕量級鎖(詳見Java併發編程的藝術-volatile的內存語義))。

2.2 基於類初始化的解決方案

JVM 在類的初始化階段,即類被加載後,被線程使用之前,會進行類的初始化,此時JVM會獲取一個對象的初始化鎖。這個鎖可以同步多個線程對同一個類的初始化。

基於該特性,可以實現第2種解決方案:

public class InstanceFactory {
	private static class InstanceHolder {
		// 類中聲明的一個靜態字段被賦值
		public static Instance instance = new Instance();
	}
	public static Instance getInstance() {
		// InstanceHolder 類中聲明的靜態字段被使用,且該字段非常量字段,InstanceHolder 類
		// 將立即初始化
		return InstanceHolder.instance;
	}
}

在多線程環境下,多個線程可能在同一時間去初始化同一個類或接口。
而 Java 語言規範規定,對於每個類C,都有一個唯一的初始化鎖LC與之對應(具體的映射由JVM實現)。JVM 在類初始化期間會獲取這個初始化鎖,且每個線程至少獲取一次鎖來確保這個類已經被初始化。

類的初始化示意圖如下:在這裏插入圖片描述



參考:
1.《Java併發編程的藝術》,方騰飛,魏鵬,程曉明著。

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