volatile關(guān)鍵字是Java虛擬機(jī)提供的的最輕量級的同步機(jī)制瞻惋,它作為一個(gè)修飾符括袒,用來修飾變量次兆。它保證變量對所有線程可見性,禁止指令重排锹锰,但是不保證原子性芥炭。
Java 內(nèi)存模型/JMM(Java Memory Model)
Java 內(nèi)存模型(JSR-133)屏蔽了硬件、操作系統(tǒng)的差異恃慧,實(shí)現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的并發(fā)效果园蝠,規(guī)定了一個(gè)線程如何和何時(shí)可以看到由其他線程修改過后的共享變量的值,以及在必須時(shí)如何同步的訪問共享變量痢士,JMM使用內(nèi)存屏障提供了java程序運(yùn)行時(shí)統(tǒng)一的內(nèi)存模型彪薛。
JMM 最重要的三點(diǎn)內(nèi)容:重排序、原子性、內(nèi)存可見性陪汽。
原子性
由Java內(nèi)存模型來直接保證的原子性變量操作包括read训唱、load、use挚冤、assign、store赞庶、write這六個(gè)训挡, 我們大致可以認(rèn)為,基本數(shù)據(jù)類型的訪問歧强、讀取和賦值都是具備原子性的澜薄。
內(nèi)存可見性
可見性就是指當(dāng)一個(gè)線程修改了共享變量的值時(shí),其他線程能夠立即得知這個(gè)修改摊册。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存肤京,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實(shí)現(xiàn)可見性的。
重排序
指令重排序是JVM為了優(yōu)化指令茅特,提高程序運(yùn)行效率忘分,在不影響 單線程程序 執(zhí)行結(jié)果的前提下,盡可能地提高并行度白修。編譯器妒峦、處理器也遵循這樣一個(gè)目標(biāo)。注意是單線程兵睛。多線程的情況下指令重排序就會(huì)給程序帶來問題肯骇。編譯器和處理器都有可能優(yōu)化執(zhí)行順序。
volatile關(guān)鍵字是怎么解決內(nèi)存可見性和重排序的祖很?
操作系統(tǒng)層面
CPU層提供了兩種解決方案:總線鎖和緩存一致性(緩存鎖)笛丙。
1、總線鎖
前端總線(也叫CPU總線)是所有CPU與芯片組連接的主干道假颇,負(fù)責(zé)CPU與外界所有部件的通信胚鸯,包括高速緩存、內(nèi)存拆融、北橋蠢琳,其控制總線向各個(gè)部件發(fā)送控制信號、通過地址總線發(fā)送地址信號指定其要訪問的部件镜豹、通過數(shù)據(jù)總線雙向傳輸傲须。
比如CPU1要操作共享內(nèi)存數(shù)據(jù)時(shí),先在總線上發(fā)出一個(gè)LOCK#信號趟脂,其他處理器就不能操作緩存了該共享變量內(nèi)存地址的緩存泰讽,也就是阻塞了其他CPU,使該處理器可以獨(dú)享此共享內(nèi)存。
很顯然已卸,這樣的做法代價(jià)十分昂貴佛玄,于是為了降低鎖粒度,CPU引入了緩存鎖累澡。
2梦抢、緩存一致性(緩存鎖)
緩存一致性:緩存一致性機(jī)制整體來說,就是當(dāng)某塊CPU對緩存中的數(shù)據(jù)進(jìn)行操作了之后愧哟,會(huì)通知其他CPU放棄儲存在它們內(nèi)部的緩存奥吩,或者從主內(nèi)存中重新讀取。
緩存鎖的核心機(jī)制就是基于緩存一致性協(xié)議來實(shí)現(xiàn)的蕊梧,即一個(gè)處理器的緩存回寫到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存無效霞赫,IA-32處理器和Intel 64處理器使用MESI實(shí)現(xiàn)緩存一致性協(xié)議。
緩存一致性是一個(gè)協(xié)議肥矢,不同處理器的具體實(shí)現(xiàn)會(huì)有所不同端衰,MESI是一種比較常見的緩存一致性協(xié)議實(shí)現(xiàn)。
緩存一致性協(xié)議的實(shí)現(xiàn)方式
寫緩存(Store Buffer)
基于存儲緩存甘改,CPU將要寫入內(nèi)存數(shù)據(jù)先寫入Store Bufferes中旅东,同時(shí)發(fā)送消息,然后就可以繼續(xù)處理其他指令了楼誓。當(dāng)收到所有其他CPU的失效確認(rèn)(Invalidate Acknowledge)時(shí)玉锌,數(shù)據(jù)才會(huì)最終被提交∨备可以理解為異步寫入不需要等待其他CPU響應(yīng)結(jié)果主守。
舉例說明一下Store Bufferes的執(zhí)行流程:比如將內(nèi)存中共享變量a的值由1修改為66。
第一步榄融,CPU-0把a(bǔ)=66寫入Store Bufferes中参淫,然后發(fā)送Invalid消息給其他CPU,無需等待其他CPU相應(yīng)愧杯,便可繼續(xù)執(zhí)行其他指令了涎才。
第二步,當(dāng)CPU-0收到其他所有CPU對Invalid通知的相應(yīng)之后力九,再把Store Bufferes中的共享變量同步到緩存和主內(nèi)存中耍铜。
Store Forward(存儲轉(zhuǎn)發(fā))
Store Bufferes的引入提升了CPU的利用效率,但又帶來了新的問題跌前。在上述第一步中棕兼,Store Bufferes中的數(shù)據(jù)還未同步到CPU-0自己的緩存中,如果此時(shí)CPU-0需要讀取該變量a抵乓,緩存中的數(shù)據(jù)并不是最新的伴挚,所以CPU需要先讀取Store Bufferes中是否有值靶衍。如果有則直接讀取,如果沒有再到自己緩存中讀取茎芋,這就是所謂的”Store Forward“颅眶。
無效化隊(duì)列(Invalidate Queue)
CPU將數(shù)據(jù)寫入Store Bufferes的同時(shí)還會(huì)發(fā)消息給其他CPU,由于Store Bufferes空間較小田弥,且其他CPU可能正在處理其他事情涛酗,沒辦法及時(shí)回復(fù),這個(gè)消息就會(huì)陷入等待偷厦。
為了避免接收消息的CPU無法及時(shí)處理Invalid失效數(shù)據(jù)的消息煤杀,造成CPU指令等待,就在接收CPU中添加了一個(gè)異步消息隊(duì)列沪哺。消息發(fā)送方將數(shù)據(jù)失效消息發(fā)送到這個(gè)隊(duì)列中,接收CPU返回已接收酌儒,發(fā)送方CPU就可以繼續(xù)執(zhí)行后續(xù)操作了辜妓。而接收方CPU再慢慢處理”失效隊(duì)列“中的消息。
由于緩存一致性協(xié)議可能還會(huì)存在亂序問題忌怎,所有就需要通過內(nèi)存屏障來解決亂序問題籍滴。
使用內(nèi)存屏障后,寫入數(shù)據(jù)時(shí)會(huì)保證所有指令都執(zhí)行完畢榴啸,這樣就能保證修改過的數(shù)據(jù)能夠即時(shí)暴露給其他CPU孽惰。而讀取數(shù)據(jù)時(shí),能夠保證所有“失效隊(duì)列”消息都消費(fèi)完畢鸥印。然后勋功,CPU根據(jù)Invalid消息判斷自己緩存狀態(tài),正確讀寫數(shù)據(jù)库说。
3狂鞋、內(nèi)存屏障
CPU層面提供了三類內(nèi)存屏障:
寫屏障(Store Memory Barrier):告訴處理器在寫屏障之前將所有存儲在存儲緩存(store bufferes)中的數(shù)據(jù)同步到主內(nèi)存。也就是說當(dāng)看到Store Barrier指令潜的,就必須把該指令之前所有寫入指令執(zhí)行完畢才能繼續(xù)往下執(zhí)行骚揍。
讀屏障(Load Memory Barrier):處理器在讀屏障之后的讀操作,都在讀屏障之后執(zhí)行啰挪。也就是說在Load屏障指令之后就能夠保證后面的讀取數(shù)據(jù)指令一定能夠讀取到最新的數(shù)據(jù)信不。
全屏障(Full Memory Barrier):兼具寫屏障和讀屏障的功能。確保屏障前的內(nèi)存讀寫操作的結(jié)果提交到內(nèi)存之后亡呵,再執(zhí)行屏障后的讀寫操作抽活。
總之,內(nèi)存屏障的作用可以通過防止CPU亂序執(zhí)行來保證共享數(shù)據(jù)在多線程下的可見性政己。
Jave層面:
JVM規(guī)范的happens-before規(guī)則酌壕,一共八種掏愁,volatile變量規(guī)則:對一個(gè)volatile域的寫,happens-before于任意后續(xù)對這個(gè)volatile域的讀卵牍。說白了就是JVM規(guī)定如果一個(gè)操作happens-before另一個(gè)操作果港,那么第一個(gè)操作的執(zhí)行結(jié)果將對第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前糊昙。
HotSpot實(shí)現(xiàn)是用“內(nèi)存屏障” 的方式來防止指令被重排序辛掠,為了實(shí)現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時(shí)释牺,會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序萝衩。大多數(shù)的處理器都支持內(nèi)存屏障的指令。
- LoadLoad Barriers:示例没咙,Load1猩谊;LoadLoad;Load2祭刚,確保Load1數(shù)據(jù)的裝載先于Load2及所有后續(xù)指令的裝載牌捷;
- StoreStore Barriers:示例,Store1涡驮;StoreStore暗甥;Store2,確保Store1數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)先于Store2及所有后續(xù)存儲指令的存儲捉捅;
- LoadStore Barriers:示例撤防,Load1;LoadStore棒口;Store2寄月,確保Load1數(shù)據(jù)裝載先于Store2及所有后續(xù)存儲指令刷新到內(nèi)存;
- StoreLoad Barriers:示例陌凳,Store1剥懒;StoreLoad;Load2合敦,確保Store1數(shù)據(jù)對其他處理器變得可見(刷新到內(nèi)存)先于Load2及所有后續(xù)裝載指令的裝載初橘。StoreLoad Barriers會(huì)使該屏障之前的所有內(nèi)存訪問指令(存儲和裝載指令)完成之后,才執(zhí)行該屏障之后的內(nèi)存訪問指令充岛。
JMM內(nèi)存屏障插入策略:
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障保檐。
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障崔梗。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障夜只。
volatile的使用場景?
項(xiàng)目當(dāng)中的使用場景
1.狀態(tài)標(biāo)志蒜魄,比如我們工程中經(jīng)常用一個(gè)變量標(biāo)識程序是否啟動(dòng)扔亥、初始化完成场躯、是否停止等
2.懶漢式單例模式,我們常用的 double-check 的單例模式
各個(gè)框架當(dāng)中的使用場景
1.AQS中的state字段是用volatile修飾的旅挤,配合CAS操作做到無鎖安全修改共享變量踢关。
2.J.U.C包中的各種原子操作類,里面的操作的數(shù)據(jù)也是用volatile修飾的粘茄,配合CAS操作做到無鎖安全修改共享變量签舞。
3.線程池中的ctl字段,也就是線程狀態(tài)+線程數(shù)量字段柒瓣。
基本上有CAS操作的屬性都需要用volatile修飾儒搭。volatile(可見性、有序性)+CAS(原子性)
參考文章: