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