如何保證內(nèi)存可見(jiàn)性?
在java虛擬機(jī)的內(nèi)存模型中,有主內(nèi)存和工作內(nèi)存的概念,每個(gè)線程對(duì)應(yīng)一個(gè)工作內(nèi)存,并共享主內(nèi)存的數(shù)據(jù),下面看看操作普通變量和volatile變量有什么不同:
1禁添、對(duì)于普通變量:讀操作會(huì)優(yōu)先讀取工作內(nèi)存的數(shù)據(jù),如果工作內(nèi)存中不存在桨踪,則從主內(nèi)存中拷貝一份數(shù)據(jù)到工作內(nèi)存中;寫(xiě)操作只會(huì)修改工作內(nèi)存的副本數(shù)據(jù)老翘,這種情況下,其它線程就無(wú)法讀取變量的最新值锻离。
2铺峭、對(duì)于volatile變量,讀操作時(shí)JMM會(huì)把工作內(nèi)存中對(duì)應(yīng)的值設(shè)為無(wú)效汽纠,要求線程從主內(nèi)存中讀取數(shù)據(jù);寫(xiě)操作時(shí)JMM會(huì)把工作內(nèi)存中對(duì)應(yīng)的數(shù)據(jù)刷新到主內(nèi)存中卫键,這種情況下,其它線程就可以讀取變量的最新值虱朵。
什么是內(nèi)存屏障?
內(nèi)存屏障莉炉,又稱內(nèi)存柵欄,是一個(gè)CPU指令碴犬。在程序運(yùn)行時(shí)絮宁,為了提高執(zhí)行性能,編譯器和處理器會(huì)對(duì)指令進(jìn)行重排序服协,JMM為了保證在不同的編譯器和CPU上有相同的結(jié)果绍昂,通過(guò)插入特定類型的內(nèi)存屏障來(lái)禁止特定類型的編譯器重排序和處理器重排序,插入一條內(nèi)存屏障會(huì)告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序蚯涮。
jmm是java內(nèi)存的訪問(wèn)模型
在程序運(yùn)行過(guò)程中治专,所有的變更會(huì)先在寄存器或本地cache中完成,然后才會(huì)被拷貝到主存以跨越內(nèi)存柵欄遭顶,此種跨越序列或順序稱為happens-before。
Happens-Before? Memory Model : 先行發(fā)生模型
定義:
第一個(gè)規(guī)則是為程序員提供的保證泪蔫,在同一個(gè)線程內(nèi)棒旗,前面的操作結(jié)果對(duì)后面的程序可見(jiàn)。?
第二個(gè)規(guī)則是向CPU提出的要求,在保證執(zhí)行結(jié)果相同的情況下铣揉,可以做程序的重排序饶深,來(lái)優(yōu)化程序的運(yùn)行。
由于指令重拍序,jmm需要提供:
原子性
可見(jiàn)性
有序性
happens-before原則規(guī)則:
程序次序規(guī)則:一個(gè)線程內(nèi)逛拱,按照代碼順序敌厘,書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作;
鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖額lock操作朽合;
volatile變量規(guī)則:對(duì)一個(gè)變量的寫(xiě)操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作俱两;
傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C曹步,則可以得出操作A先行發(fā)生于操作C宪彩;
線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作;
線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生讲婚;
線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè)尿孔,我們可以通過(guò)Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行筹麸;
對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開(kāi)始活合;
程序次序規(guī)則:一段代碼在單線程中執(zhí)行的結(jié)果是有序的。注意是執(zhí)行結(jié)果物赶,因?yàn)樘摂M機(jī)白指、處理器會(huì)對(duì)指令進(jìn)行重排序(重排序后面會(huì)詳細(xì)介紹)。雖然重排序了块差,但是并不會(huì)影響程序的執(zhí)行結(jié)果侵续,所以程序最終執(zhí)行的結(jié)果與順序執(zhí)行的結(jié)果是一致的。故而這個(gè)規(guī)則只對(duì)單線程有效憨闰,在多線程環(huán)境下無(wú)法保證正確性状蜗。
鎖定規(guī)則:這個(gè)規(guī)則比較好理解,無(wú)論是在單線程環(huán)境還是多線程環(huán)境鹉动,一個(gè)鎖處于被鎖定狀態(tài)轧坎,那么必須先執(zhí)行unlock操作后面才能進(jìn)行l(wèi)ock操作。
volatile變量規(guī)則:這是一條比較重要的規(guī)則泽示,它標(biāo)志著volatile保證了線程可見(jiàn)性缸血。通俗點(diǎn)講就是如果一個(gè)線程先去寫(xiě)一個(gè)volatile變量,然后一個(gè)線程去讀這個(gè)變量械筛,那么這個(gè)寫(xiě)操作一定是happens-before讀操作的捎泻。
傳遞規(guī)則:提現(xiàn)了happens-before原則具有傳遞性,即A happens-before B , B happens-before C埋哟,那么A happens-before C
線程啟動(dòng)規(guī)則:假定線程A在執(zhí)行過(guò)程中笆豁,通過(guò)執(zhí)行ThreadB.start()來(lái)啟動(dòng)線程B,那么線程A對(duì)共享變量的修改在接下來(lái)線程B開(kāi)始執(zhí)行后確保對(duì)線程B可見(jiàn)。
線程終結(jié)規(guī)則:假定線程A在執(zhí)行的過(guò)程中闯狱,通過(guò)制定ThreadB.join()等待線程B終止煞赢,那么線程B在終止之前對(duì)共享變量的修改在線程A等待返回后可見(jiàn)。
Java虛擬機(jī)內(nèi)存模型中定義了8種關(guān)于主內(nèi)存和工作內(nèi)存的交互協(xié)議操作:
lock:作用于主內(nèi)存的變量哄孤,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)照筑。
unlock:作用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái)瘦陈,釋放后的變量可以被其他線程鎖定凝危。
read:作用于主內(nèi)的變量,把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中双饥,以便隨后的load動(dòng)作使用媒抠。
load:作用于工作內(nèi)存的變量,把read讀取操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量拷貝中咏花。
use:作用于工作內(nèi)存的變量趴生,把工作內(nèi)存中一個(gè)變量的值傳遞給java虛擬機(jī)執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行該操作昏翰。
assign:作用于工作內(nèi)存變量苍匆,把一個(gè)從執(zhí)行引擎接收到的變量的值賦值給工作變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼時(shí)將會(huì)執(zhí)行該操作棚菊。
store:作用于工作內(nèi)存的變量浸踩,把工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write操作使用统求。
write:作用于主內(nèi)存的變量检碗,把store操作從工作內(nèi)存中得到的變量值放入主內(nèi)存的變量中。
內(nèi)存柵欄
主線程對(duì)done做了更改码邻,但是新的線程無(wú)法看到該變量值得變化(內(nèi)存柵欄)折剃,認(rèn)為done還是false。
volatile原語(yǔ)能夠向JIT發(fā)出警告像屋,done變量可能會(huì)被某個(gè)線程更改怕犁,對(duì)該變量的任何讀寫(xiě)都應(yīng)該忽視本地cache并直接對(duì)內(nèi)存進(jìn)行操作。
當(dāng)一個(gè)變量被聲明為volatile之后己莺,JMM對(duì)其做了特殊規(guī)則:
volatile變量的操作必須嚴(yán)格按load->use順序奏甫,前一個(gè)動(dòng)作是load時(shí)才能執(zhí)行use動(dòng)作,后一個(gè)動(dòng)作是use時(shí)才能執(zhí)行l(wèi)oad動(dòng)作凌受,即每次在工作內(nèi)存中使用變量前必須先從主內(nèi)存中刷新最新的值阵子,以保證能看到其他線程對(duì)變量的最新修改。
volatile變量的操作必須嚴(yán)格按assign->store順序胜蛉,前一個(gè)動(dòng)作是assign時(shí)才能執(zhí)行store動(dòng)作款筑,后一個(gè)動(dòng)作是store時(shí)才能執(zhí)行assign動(dòng)作智蝠,即每次在工作內(nèi)存為變量賦值之后必須將變量的值同步回主內(nèi)存腾么,以保證讓其他線程能看到變量的最新修改奈梳。
若線程對(duì)volatile變量V的assign或者use操作先于對(duì)volatile變量W的assign或者use操作,則線程對(duì)volatile變量A的read/load或者store/write操作也必定先于對(duì)volatile變量B的read/load或者store/write操作解虱。
在Java中包含了幾個(gè)關(guān)鍵字:volatile攘须、final和synchronized
synchronization
互斥,對(duì)于一個(gè)monitor對(duì)象殴泰,只能夠被一個(gè)線程持有
synchronization保證了線程在同步塊之前或者期間寫(xiě)入動(dòng)作于宙,對(duì)于后續(xù)進(jìn)入該代碼塊的線程是可見(jiàn)的,在一個(gè)線程退出同步塊時(shí),線程釋放monitor對(duì)象悍汛,它的作用是把CPU緩存數(shù)據(jù)(本地緩存數(shù)據(jù))刷新到主內(nèi)存中捞魁,從而實(shí)現(xiàn)該線程的行為可以被其它線程看到。在其它線程進(jìn)入到該代碼塊時(shí)离咐,需要獲得monitor對(duì)象,它在作用是使CPU緩存失效,從而使變量從主內(nèi)存中重新加載突委,然后就可以看到之前線程對(duì)該變量的修改瘟斜。
禁止指令的重排序,對(duì)于編譯器來(lái)說(shuō)术陶,同步塊中的代碼不會(huì)移動(dòng)到獲取和釋放monitor外面凑懂。
final?
正確的構(gòu)造一個(gè)對(duì)象后,final字段被設(shè)置后對(duì)于其它線程是可見(jiàn)的梧宫。正確構(gòu)造對(duì)象接谨,意思是在對(duì)象的構(gòu)造過(guò)程中,不允許對(duì)該對(duì)象進(jìn)行引用塘匣,不然的話脓豪,可能存在其它線程在對(duì)象還沒(méi)構(gòu)造完成時(shí)就對(duì)該對(duì)象進(jìn)行訪問(wèn),造成不必要的麻煩馆铁。
volatile
必須保證在被寫(xiě)入之后跑揉,會(huì)被刷新到主內(nèi)存中,這樣就可以立即對(duì)其它線程可以見(jiàn)
volatile禁止這兩個(gè)寫(xiě)入行為的重排序