摘要:但是有引入了新的問題線程不安全,返回的對象可能還沒有初始化。如果只有一個線程調用是沒有問題的因為不管步驟如何調換,保證返回的對象是已經構造好了。這種特殊情況稱之為指令重排序采用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理。
雙重檢測鎖的演變過程 synchronized修飾方法的單例模式目錄
雙重檢測鎖的演變過程
利用HappensBefore分析并發問題
無volatile的雙重檢測鎖
雙重檢測鎖的最初形態是通過在方法聲明的部分加上synchronized進行同步,保證同一時間調用方法的線程只有一個,從而保證new Singlton()的線程安全:
public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
這樣做的好處是代碼簡單、并且JVM保證new Singlton()這行代碼線程安全。但是付出的代價有點高昂:
所有的線程的每一次調用都是同步調用,性能開銷很大,而且new Singlton()只會執行一次,不需要每一次都進行同步。
既然只需要在new Singlton()時進行同步,那么把synchronized的同步范圍縮小呢?
線程不安全的雙重檢測鎖public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
把synchronized同步的范圍縮小以后,貌似是解決了每次調用都需要進行同步而導致的性能開銷的問題。但是有引入了新的問題:線程不安全,返回的對象可能還沒有初始化。
深入到字節碼的層面來看看下面這段代碼:
instance = new Singleton() returen instance;
正常情況下JVM編譯成成字節碼,它是這樣的:
step.1 new:開辟一塊內存空間 step.2 invokespecial:執行初始化方法,對內存進行初始化 step.3 putstatic:將該內存空間的引用賦值給instance step.4 areturn:方法執行結束,返回instance
當然這里限定在正常情況下,在特殊情況下也可以編譯成這樣:
step.1 new:開辟一塊內存空間 step.3 putstatic:將該內存空間的引用賦值給instance step.2 invokespecial:執行初始化方法,對內存進行初始化 step.4 areturn:方法執行結束,返回instance
步驟2和步驟3進行了調換:先執行步驟3再執行步驟2。
如果只有一個線程調用是沒有問題的:因為不管步驟如何調換,JVM保證返回的對象是已經構造好了。
如果同時有多個線程調用,那么部分調用線程返回的對象有可能是沒有構造好的對象。
這種特殊情況稱之為:指令重排序:CPU采用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理。當然不是亂排序,重排序保證CPU能夠正確處理指令依賴情況以保障程序能夠得出正確的執行結果。
利用HappensBefore分析并發問題 什么是HappensBeforeHappensBefore:先行發生,是
判斷數據是否存在競爭、線程是否安全的重要依據
A happens-beforeB,那么A對B可見(A做的操作對B可見)
是一種偏序關系。hb(a,b),hb(b,c) => hb(a,c)
換句話說,可以通過HappensBefore推斷代碼在多線程下是否線程安全
舉一個《深入理解Java虛擬機》上的例子:
//以下操作在線程A中執行 int i = 1; //以下操作在線程B中執行 j = i; //以下操作在線程C中執行 i = 2;
如果hb(i=1,j=i),那么可以確定變量j的值一定等于1。得出這個結論的依據有兩個:
根據HappensBefore的規則,i=1的結果可以被j=i觀察到
線程C還沒有登場
如果線程C的執行時間在線程A和線程B之間,那么j的值是多少呢?答案是不確定!因為線程C和線程B之間沒有HappensBefore的關系:線程C對變量的i的更改可能被線程B觀察到也可能不會!
HappensBefore關系這些是“天然的”、JVM保證的HappensBefore關系:
程序次序規則
管程鎖定規則
volatile變量規則
線程啟動規則
線程終止規則
線程中斷規則
對象終結規則
傳遞性
重點介紹程序次序規則,管程鎖定規則,volatile變量規則,傳遞性,后面分析需要用到這四個性質:
程序次序規則:在一個線程內,按照程序控制流順序,書寫在前面的操作HappensBefore書寫在后面的操作
管程鎖定規則:對于同一個鎖來說,在時間順序上,上一個unlock操作HappensBefore下一個lock操作
volatile變量規則:對于一個volatile修飾的變量,在時間順序上,寫操作HappensBefore讀操作
傳遞性:hb(a,b),hb(b,c) => hb(a,c)
分析之前線程不安全的雙重檢測鎖public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { //1 synchronized (Singleton.class) { //2 if (instance == null) { //3 instance = new Singleton(); //4 new //4.1 invokespecial //4.2 pustatic //4.3 } } } return instance; //5 } }
經過上面的討論,已經知道因為JVM重排序導致代碼4.2提前執行了,導致后面一個線程執行代碼1返回的值為false,進而直接返回了還沒有構造好的instance對象:
線程1 | 線程2 |
---|---|
1 | |
2 | |
3 | |
4.1 | |
4.3 | |
1 | |
5 | |
4.2 | |
5 |
通過表格,可能清晰看到問題所在:線程1代碼4.3 執行后,線程2執行代碼1讀到了臟數據。要想不讀到臟數據,只要證明存在hb(T1-4.3,T2-1)(T1-4表示線程1代碼4,T2-1表示線程2代碼1,下同),那么是否存在呢?很遺憾,不存在:
程序次序規則:不在同一個線程
管程鎖定規則:線程2沒有嘗試lock
volatile變量規則:instance對象沒有通過volatile關鍵字修飾
傳遞性:不存在
用HappensBefore分析,可以很清晰、明確看到沒有volatile修飾的雙重檢測鎖是線程不安全的。但,真的是這樣的嗎?
無volatile的雙重檢測鎖在第二部分,通過HappensBefore分析沒有volatile修飾的雙重檢測鎖是線程不安全,那只有用volatile修飾的雙重檢測鎖才是線程安全的嗎?答案是否定的。
用volatile關鍵字修飾的本質是想利用volatile變量規則,使得寫操作(T1-4)HappensBefore讀操作(T2-1),那只要另找一條HappensBefore規則保證即可。答案是程序次序規則和管程鎖定規則
先看代碼:
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { //1 synchronized (Singleton.class) { //2 if (instance == null) { //3 Singleton temp = new Singleton(); //4 temp.toString(); //5 instance = temp; //6 } } } return instance; //7 } }
在原有的基礎上加了兩行代碼:
instance = new Singleton(); //4 Singleton temp = new Singleton(); //4 temp.toString(); //5 instance = temp; //6
為什么要這么做?
通過管程鎖定規則保證執行到代碼6時,temp對象已經構造好了。想一想,為什么?
其他線程執行代碼1時,如果能夠觀察到T1-6的寫操作,那么直接返回instance對象
如果沒有觀察到T1-6的寫操作,那么嘗試獲取鎖,此時管程鎖定規則開始生效:保證當前線程一定能夠觀察到T1-6操作
執行流程可能是這樣的:
線程1 | 線程2 | 線程3 |
---|---|---|
1 | ||
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
2 | ||
3 | ||
1 | 7 | |
7 | ||
7 |
無論怎樣執行,其他線程都能夠觀察到T1-6的寫操作
其他 volatile、synchronized為什么可以禁止JVM重排序內存屏障。
JVM在凡是有volatile、synchronized出現的地方都加了一道內存屏障:重排序時,不可以把內存屏障后面的指令重排序到內存屏障前面執行,并且會及時的將線程工作內存中的數據及時更新到主內存中,進而使得其他的線程能夠觀察到最新的數據
參考資料
《深入理解Java虛擬機》
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74288.html
摘要:代碼實現單例模式靜態變量保存全局實例私有構造函數,防止外界實例化對象私有克隆函數,防止外界克隆對象靜態方法,單例統一訪問路口單例模式的優缺點優點改進系統的設計是對全局變量的一種改進缺點難于調試隱藏的依賴關系無法用錯誤類型的數據覆寫一個單例 單例模式(Singleton Pattern 單件模式或單元素模式)單例模式有以下3個特點:1、一個類只能有一個類對象(只能實例化一個對象)2、它必...
摘要:代碼實現單例模式靜態變量保存全局實例私有構造函數,防止外界實例化對象私有克隆函數,防止外界克隆對象靜態方法,單例統一訪問路口單例模式的優缺點優點改進系統的設計是對全局變量的一種改進缺點難于調試隱藏的依賴關系無法用錯誤類型的數據覆寫一個單例 單例模式(Singleton Pattern 單件模式或單元素模式)單例模式有以下3個特點:1、一個類只能有一個類對象(只能實例化一個對象)2、它必...
摘要:代碼實現單例模式靜態變量保存全局實例私有構造函數,防止外界實例化對象私有克隆函數,防止外界克隆對象靜態方法,單例統一訪問路口單例模式的優缺點優點改進系統的設計是對全局變量的一種改進缺點難于調試隱藏的依賴關系無法用錯誤類型的數據覆寫一個單例 單例模式(Singleton Pattern 單件模式或單元素模式)單例模式有以下3個特點:1、一個類只能有一個類對象(只能實例化一個對象)2、它必...
摘要:的構造函數實際上負責了兩件事情。有一個缺點,假如我們某天需要利用這個類,在頁面中創建千千萬萬個,即要這個類從單例類變成一個普通的可產生多個實例的類,那我們就要改寫構造函數,把控制創建唯一對象的那一段去掉,這樣會給我們帶來不必要的麻煩。 定義:單例模式保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。 單例模式是一種常用的模式,有一些對象我們往往只需要一個,比如線程池、全局緩存、瀏覽...
閱讀 1048·2021-11-22 15:33
閱讀 3357·2021-11-08 13:20
閱讀 1368·2021-09-22 10:55
閱讀 2053·2019-08-29 11:08
閱讀 771·2019-08-26 12:24
閱讀 3068·2019-08-23 17:15
閱讀 2225·2019-08-23 16:12
閱讀 1933·2019-08-23 16:09