介紹
雙重校驗鎖是單例模式中,餓漢式的一種實現方式。因為有兩次判空校驗,所以叫雙重校驗鎖,一次是在同步代碼塊外,一次是在同步代碼塊內。
為什么在同步代碼塊內還要再檢驗一次?
第一個if減少性能開銷,第二個if避免生成多個對象實例。
現有三個線程A,B,C,假設線程A和線程B同時調用getSingleton()時,判斷第一層if判斷都為空,這時線程A先拿到鎖,線程B在代碼塊外層等待。線程A進行第二層if判斷,條件成立后new了一個新對象,創建完成,釋放鎖,線程B拿到鎖,進行第二層if判斷,singleton不為空,直接返回singleton釋放鎖,避免生成多個對象實例。線程線C調用getSingleton時第一層判斷不成立,直接拿到singleton對象返回,避免進入鎖,減少性能開銷。
為什么要用volatile關鍵字?
singleton = new Singleton();這行代碼並不是一個原子指令,可能會在JVM中進行指令重排;
new 實例背后的指令,我們通過使用 javap -c指令,查看字節碼如下:
// 創建 Singleton 對象實例,分配內存 0: new #5 // 復制棧頂地址,並再將其壓入棧頂 3: dup // 調用構造器方法,初始化 Singleton對象 4: invokespecial #6 // Method "<init>":()V // 存入局部方法變量表 7: astore_1
從字節碼可以看到創建一個對象實例,可以分為三步:
(1)分配對象內存(給singleton分配內存)。
(2)調用構造器方法,執行初始化(調用 Singleton 的構造函數來初始化成員變量)。
(3)將對象引用賦值給變量(執行完這步 singleton 就為非 null 了)。
在 JVM 的即時編譯器中存在指令重排序的優化。指令重排並不影響單線程內的執行結果,但是在多線程內可能會影響結果。也就是說上面的2和3的順序是不能保證的,但是並不會重排序 1 的順序,因為 2,3 指令需要依托 1 指令執行結果。最終的執行順序可能是 1-2-3 也可能是 1-3-2。
1-3-2的情況
上面多線程執行的流程中,如果線程A獲取到鎖進入創建對象實例,這個時候發生了指令重排序。當線程A 執行到 t3 時刻(singleton已經非null了,但是卻沒有初始化),此時線程 B 搶占了,由於此時singleton已經不為 Null,會直接返回 singleton對象,然后使用singleton對象,然而該對象還未初始化,就會報錯。我們只需將 singleton 變量聲明成 volatile 就可以禁止指令重排,避免這種現象發生。
參考/好文:
菜鳥教程 – 設計模式--https://www.runoob.com/design-pattern/singleton-pattern.html
掘金 --https://juejin.im/post/5d54c2d251882542f27bdff6