單例模式雙重檢測鎖詳解以及爲何雙重檢測

前言:

在瞭解完volatile關鍵字之後,再仔細思考了單例模式的雙重檢測,發現以前挺多東西還沒懂的。

DCL(Double Check Lock

public class Singleton {
	private volatile static Singleton uniqueInstance;
	private Singleton() {}
	public static Singleton getInstance() {
    //第一次檢測
		if (uniqueInstance == null) {
			synchronized (Singleton.class) {
        //第二次檢測
				if (uniqueInstance == null) {
					uniqueInstance = new Singleton();
				}
			}
		}
		return uniqueInstance;
	}
}

顯而易見我們都知道volatile關鍵字的作用其實就是讓該變量的變化對於每一個線程可見,其底層實現原理是由於java內存模型(jmm)中的封裝了8個交互操作。

  • read:把一個主內存中的值傳遞到工作內存,以便load動作使用
  • load:把read操作從主內存獲取的內存變量賦值到工作內存的變量副本
  • use:將工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的字節碼指令的時候將會執行這個操作。
  • assign:從執行引擎接受到的值賦給工作內存的變量,當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
  • store:他把工作內存中的一個變量的值傳送到主內存中,以便隨後的write操作使用
  • write:把store操作從工作內存中得到的變量的值放入主內存的變量中
  • lock:作用於主內存的變量,把一個變量標示一條線程獨佔的狀態。
  • Unlock:作用於主內存,把一個處於鎖定狀態的變量釋放出來。釋放後的變量纔可以被其他線程鎖定。

每次執行use操作的時候都先執行read和load操作,讓volatile修飾的變量每次獲取的都是新的值;

每次執行assign的時候,隨後都會執行store和write操作,讓volatile修飾的變量每次都刷新到主內存中。

還有一個點就是其禁止指令重排序。

uniqueInstance = new Singleton();

主要在於uniqueInstance = new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。

1. 給 uniqueInstance 分配內存
  2. 調用 Singleton 的構造函數來初始化成員變量,形成實例
  3. 將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null了)

在JVM的即時編譯器中存在指令重排序的優化。

​ 也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯(因爲沒有初始化)  
​ 再稍微解釋一下,就是說,由於有一個『instance已經不爲null但是仍沒有完成初始化』的中間狀態,而這個時候,如果有其他線程剛好運行到第一層if (instance ==null)這裏,這裏讀取到的instance已經不爲null了,所以就直接把這個中間狀態的instance拿去用了,就會產生問題。這裏的關鍵在於線程T1對instance的寫操作沒有完成,線程T2就執行了讀操作。

volatile如何解決

​ volatile關鍵字的一個作用是禁止指令重排,把uniqueInstance聲明爲volatile之後,對它的寫操作就會有一個內存屏障,這樣,在它的賦值完成之前,就不用會調用讀操作。

何爲內存屏障

觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令,此lock非jmm交互操作的lock。

lock指令的作用是使本cpu的cache寫入內存,該寫入動作會引起其他cpu的cache無效化(緩存一致性)。通過這樣一個操作讓對於volatile變量的修改對於其他cpu可變。

lock指令把之前的cache都同步到內存中,等同於讓lock指令後面的指令依賴於lock指令前面的指令,根據處理器在進行重排序時是會考慮指令之間的數據依賴性,所以lock指令之前的指令不會跑到lock指令之後,之後的也不會跑到之前。

so

volatitle解決了兩個問題:instance的線程可見性、以及在初始化instance的時候遇到的指令重排序問題。

double check的意義

爲什麼要判斷兩次instance==null呢???

第一次檢測:

​ 由於單例模式只需要創建一次實例,如果後面再次調用getInstance方法時,則直接返回之前創建的實例,因此大部分時間不需要執行同步方法裏面的代碼,大大提高了性能。如果不加第一次校驗的話,每次都要去競爭鎖。

第二次檢測:

​ 如果沒有第二次校驗,假設線程t1執行了第一次校驗後,判斷爲null,這時t2也獲取了CPU執行權,也執行了第一次校驗,判斷也爲null。接下來t2獲得鎖,創建實例。這時t1又獲得CPU執行權,由於之前已經進行了第一次校驗,結果爲null(不會再次判斷),獲得鎖後,直接創建實例。結果就會導致創建多個實例。所以需要在同步代碼裏面進行第二次校驗,如果實例爲空,則進行創建。

​ 簡單來說就是爲了防止創建多個實例。

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