摘要:文章簡(jiǎn)介分析的作用以及底層實(shí)現(xiàn)原理,這也是大公司喜歡問的問題內(nèi)容導(dǎo)航的作用什么是可見性源碼分析的作用在多線程中,和都起到非常重要的作用,是通過加鎖來實(shí)現(xiàn)線程的安全性。而的主要作用是在多處理器開發(fā)中保證共享變量對(duì)于多線程的可見性。
文章簡(jiǎn)介
分析volatile的作用以及底層實(shí)現(xiàn)原理,這也是大公司喜歡問的問題
內(nèi)容導(dǎo)航volatile的作用
什么是可見性
volatile源碼分析
volatile的作用在多線程中,volatile和synchronized都起到非常重要的作用,synchronized是通過加鎖來實(shí)現(xiàn)線程的安全性。而volatile的主要作用是在多處理器開發(fā)中保證共享變量對(duì)于多線程的可見性。
可見性的意思是,當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí),另外一個(gè)線程能讀取到修改以后的值。接下來通過一個(gè)簡(jiǎn)單的案例來演示可見性問題
public class VolatileDemo { private /*volatile*/ static boolean stop=false; //添加volatile修飾和不添加volatile修飾的演示效果 public static void main(String[] args) throws InterruptedException { Thread thread=new Thread(()->{ int i=0; while(!stop){ i++; } }); thread.start(); System.out.println("begin start thread"); Thread.sleep(1000); stop=true; } }
定義一個(gè)共享變量 stop
在main線程中創(chuàng)建一個(gè)子線程 thread,子線程讀取到 stop的值做循環(huán)結(jié)束的條件
main線程中修改stop的值為 true
當(dāng) stop沒有增加volatile修飾時(shí),子線程對(duì)于主線程的 stop=true的修改是不可見的,這樣將導(dǎo)致子線程出現(xiàn)死循環(huán)
當(dāng) stop增加了volatile修飾時(shí),子線程可以獲取到主線程對(duì)于 stop=true的值,子線程while循環(huán)條件不滿足退出循環(huán)
增加volatile關(guān)鍵字以后,main線程對(duì)于共享變量 stop值的更新,對(duì)于子線程 thread可見,這就是volatile的作用
這段代碼有些人測(cè)試不出效果,是因?yàn)镴VM沒有優(yōu)化導(dǎo)致的,在cmd控制臺(tái)輸入java -version,如果顯示的是 JavaHotSpot(TM)ServerVM,就能正常演示,如果是 JavaHotSpot(TM)ClientVM,需要設(shè)置成 Server模式
什么是可見性,以及volatile是如何保證可見性的呢?
什么是可見性在并發(fā)編程中,線程安全問題的本質(zhì)其實(shí)就是 原子性、有序性、可見性;接下來主要圍繞這三個(gè)問題進(jìn)行展開分析其本質(zhì),徹底了解可見性的特性
原子性 和數(shù)據(jù)庫事務(wù)中的原子性一樣,滿足原子性特性的操作是不可中斷的,要么全部執(zhí)行成功要么全部執(zhí)行失敗
有序性 編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重排序,也就是你編寫的代碼順序和最終執(zhí)行的指令順序是不一致的,重排序可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題
可見性 多個(gè)線程訪問同一個(gè)共享變量時(shí),其中一個(gè)線程對(duì)這個(gè)共享變量值的修改,其他線程能夠立刻獲得修改以后的值
為了徹底了解這三個(gè)特性,我們從兩個(gè)層面來分析,第一個(gè)層面是硬件層面、第二個(gè)層面是JMM層面
從硬件層面分析三大特性原子性、有序性、可見性這些問題,我們可以認(rèn)為是基于多核心CPU架構(gòu)下的存在的問題。因?yàn)樵趩魏薈PU架構(gòu)下,所有的線程執(zhí)行都是基于CPU時(shí)間片切換,所以不存在并發(fā)問題 (在IntelPentium4開始,引入了超線程技術(shù),也就是一個(gè)CPU核心模擬出2個(gè)線程的CPU,實(shí)現(xiàn)多線程并行)。
CPU高速緩存線程設(shè)計(jì)的目的是充分利用CPU達(dá)到實(shí)時(shí)性的效果,但是很多時(shí)候CPU的計(jì)算任務(wù)還需要和內(nèi)存進(jìn)行交互,比如讀取內(nèi)存中的運(yùn)算數(shù)據(jù)、將處理結(jié)果寫入到內(nèi)存。在理想情況下,存儲(chǔ)器應(yīng)該是非常快速的執(zhí)行一條指令,這樣CPU就不會(huì)受到存儲(chǔ)器的限制。但目前技術(shù)無法滿足,所以就出現(xiàn)了其他的處理方式。
存儲(chǔ)器頂層是CPU中的寄存器,存儲(chǔ)容量小,但是速度和CPU一樣快,所以CPU在訪問寄存器時(shí)幾乎沒有延遲;接下來就是CPU的高速緩存;最后就是內(nèi)存。
高速緩存從下到上越接近CPU訪問速度越快,同時(shí)容量也越小。現(xiàn)在的大部分處理器都有二級(jí)或者三級(jí)緩存,分別是L1/L2/L3, L1又分為L(zhǎng)1-d的數(shù)據(jù)緩存和L1-i的指令緩存。其中L3緩存是在多核CPU之間共享的。
原子性在多核CPU架構(gòu)下,在同一時(shí)刻對(duì)同一共享變量執(zhí)行 decl指令(遞減指令,相當(dāng)于i--,它分為三個(gè)過程:讀->改->寫,這個(gè)指令涉及到兩次內(nèi)存操作,那么在這種情況下i的結(jié)果是無法預(yù)測(cè)的。這就是原子性問題
處理器如何解決原子性問題呢?
其實(shí)這個(gè)問題稍微提煉一下,無非就是多線程并行訪問同一個(gè)共享資源的時(shí)候的原子性問題,如果把問題放大到分布式架構(gòu)里面,這個(gè)問題的解決方法就是鎖。所以在CPU層面,提供了兩種鎖的機(jī)制來保證原子性
總線鎖如果多個(gè)處理器同時(shí)對(duì)同一共享變量進(jìn)行 decl指令操作,那這個(gè)操作一定不是原子的,也就是執(zhí)行的結(jié)果和預(yù)期結(jié)果不一致。如下圖所示,我們期望的結(jié)果是3,但是有可能結(jié)果是2
如果要解決這個(gè)問題,就需要是的CPU0在更新共享變量時(shí),CPU1就不能操作緩存了該共享變量?jī)?nèi)存地址的緩存,所以處理器提供了總線鎖來解決問題,處理器會(huì)提供一個(gè)LOCK#信號(hào),當(dāng)一個(gè)處理器在總線上輸出這個(gè)信號(hào)時(shí),其他處理器的請(qǐng)求會(huì)被阻塞,那么該處理器就可以獨(dú)占共享內(nèi)存
總線鎖有一個(gè)弊端,總線鎖相當(dāng)于使得多個(gè)CPU由并行執(zhí)行變成了串行,使得CPU的性能嚴(yán)重下降,所以在P6系列以后的處理器中,引入了緩存鎖。緩存鎖
我們只需要保證 多個(gè)線程操作同一個(gè)被緩存的共享數(shù)據(jù)的原子性就行,所以只需要鎖定被緩存的共享對(duì)象即可。所謂緩存鎖是指被緩存在處理器中的共享數(shù)據(jù),在Lock操作期間被鎖定,那么當(dāng)被修改的共享內(nèi)存的數(shù)據(jù)回寫到內(nèi)存時(shí),處理器不在總線上聲明LOCK#信號(hào),而是修改內(nèi)部的內(nèi)存地址,并通過 緩存一致性機(jī)制來保證操作的原子性。
什么是緩存一致性呢?
所謂緩存一致性,就是多個(gè)CPU核心中緩存的同一共享數(shù)據(jù)的數(shù)據(jù)一致性,而(MESI)使用比較廣泛的緩存一致性協(xié)議。MESI協(xié)議實(shí)際上是表示緩存的四種狀態(tài)
M(Modify) 表示共享數(shù)據(jù)只緩存在當(dāng)前CPU緩存中,并且是被修改狀態(tài),也就是緩存的數(shù)據(jù)和主內(nèi)存中的數(shù)據(jù)不一致
E(Exclusive) 表示緩存的獨(dú)占狀態(tài),數(shù)據(jù)只緩存在當(dāng)前CPU緩存中,并且沒有被修改
S(Shared) 表示數(shù)據(jù)可能被多個(gè)CPU緩存,并且各個(gè)緩存中的數(shù)據(jù)和主內(nèi)存數(shù)據(jù)一致
I(Invalid) 表示緩存已經(jīng)失效
每個(gè)CPU核心不僅僅知道自己的讀寫操作,也會(huì)監(jiān)聽其他Cache的讀寫操作
CPU的讀取會(huì)遵循幾個(gè)原則
如果緩存的狀態(tài)是I,那么就從內(nèi)存中讀取,否則直接從緩存讀取
如果緩存處于M或者E的CPU 嗅探到其他CPU有讀的操作,就把自己的緩存寫入到內(nèi)存,并把自己的狀態(tài)設(shè)置為S
只有緩存狀態(tài)是M或E的時(shí)候,CPU才可以修改緩存中的數(shù)據(jù),修改后,緩存狀態(tài)變?yōu)镸
可見性CPU高速緩存以及指令重排序都會(huì)造成可見性問題,接下來從兩個(gè)角度來分析
MESI優(yōu)化帶來的可見性問題前面說過MESI協(xié)議,也就是緩存一致性協(xié)議。這個(gè)協(xié)議存在一個(gè)問題,就是當(dāng)CPU0修改當(dāng)前緩存的共享數(shù)據(jù)時(shí),需要發(fā)送一個(gè)消息給其他緩存了相同數(shù)據(jù)的CPU核心,這個(gè)消息傳遞給其他CPU核心以及收到消息完成各自緩存狀態(tài)的切換這個(gè)過程中,CPU會(huì)等待所有緩存響應(yīng)完成,這樣會(huì)降低處理器的性能。為了解決這個(gè)問題,引入了 StoreBufferes存儲(chǔ)緩存。
處理器把需要寫入到主內(nèi)存中的值先寫入到存儲(chǔ)緩存中,然后繼續(xù)去處理其他指令。當(dāng)所有的CPU核心返回了失效確認(rèn)時(shí),數(shù)據(jù)才會(huì)被最終提交。但是這種優(yōu)化又會(huì)帶來另外的問題。
如果某個(gè)CPU嘗試將其他CPU占有的共享數(shù)據(jù)寫入到內(nèi)存,消息提交給store buffer以后,當(dāng)前CPU繼續(xù)做其他事情,而如果后面的指令依賴于這個(gè)被寫入內(nèi)存的最新數(shù)據(jù)(由于store buffer還沒有寫入到內(nèi)存),就會(huì)產(chǎn)生可見性問題(也就是值還沒有更新到內(nèi)存中,這個(gè)時(shí)候讀取到的共享數(shù)據(jù)的值是錯(cuò)誤的)。
Store Bufferes中的數(shù)據(jù)何時(shí)寫入到內(nèi)存中是不確定的,那么意味著這個(gè)過程的執(zhí)行順序也是不確定的,比如下面這個(gè)例子
exeToCPU0和exeToCPU1分別在兩個(gè)獨(dú)立的cpu核心上執(zhí)行,假如CPU0 緩存了 isFinish這個(gè)共享變量,并且狀態(tài)為(E->獨(dú)占),而value可能是(S共享狀態(tài)被其他CPU核心修改以后變?yōu)镮(失效狀態(tài))。
這種情況下value的緩存數(shù)據(jù)變更路徑為, value將失效狀態(tài)需要響應(yīng)給觸發(fā)緩存更新的CPU核心,接著該CPU將 StoreBufferes寫入到內(nèi)存,這就會(huì)導(dǎo)致value會(huì)比isFinish更遲的拋棄存儲(chǔ)緩存。那么就可能出現(xiàn)CPU1讀取到了isFinish的值為true,而value的值不等于10的情況。
這種CPU的內(nèi)存亂序訪問,會(huì)帶來可見性問題。
value = 3; void exeToCPU0(){ value = 10; isFinsh = true; } void exeToCPU1(){ if(isFinsh){ assert value == 10; } }CPU層面的內(nèi)存屏障
什么是內(nèi)存屏障?從前面的內(nèi)容基本能有一個(gè)初步的猜想,內(nèi)存屏障就是將 store bufferes中的指令寫入到內(nèi)存,從而使得其他訪問同一共享內(nèi)存的線程的可見性。
X86的memory barrier指令包括lfence(讀屏障) sfence(寫屏障) mfence(全屏障)
Store Memory Barrier(寫屏障) 告訴處理器在寫屏障之前的所有已經(jīng)存儲(chǔ)在存儲(chǔ)緩存(store bufferes)中的數(shù)據(jù)同步到主內(nèi)存,簡(jiǎn)單來說就是使得寫屏障之前的指令的結(jié)果對(duì)屏障之后的讀或者寫是可見的
Load Memory Barrier(讀屏障) 處理器在讀屏障之后的讀操作,都在讀屏障之后執(zhí)行。配合寫屏障,使得寫屏障之前的內(nèi)存更新對(duì)于讀屏障之后的讀操作是可見的
Full Memory Barrier(全屏障) 確保屏障前的內(nèi)存讀寫操作的結(jié)果提交到內(nèi)存之后,再執(zhí)行屏障后的讀寫操作
有了內(nèi)存屏障以后,對(duì)于上面這個(gè)例子,我們可以這么來改,從而避免出現(xiàn)可見性問題
value = 3; void exeToCPU0(){ value = 10; storeMemoryBarrier(); //這個(gè)是一個(gè)偽代碼,插入一個(gè)寫屏障,使得value=10這個(gè)值強(qiáng)制寫入到主內(nèi)存中 isFinsh = true; } void exeToCPU1(){ if(isFinsh){ loadMemoryBarrier();//偽代碼,插入一個(gè)讀屏障,使得cpu1從主內(nèi)存中獲得最新的數(shù)據(jù) assert value == 10; } }
總的來說,內(nèi)存屏障的作用可以通過防止CPU對(duì)內(nèi)存的亂序訪問來保證共享數(shù)據(jù)在多線程并行執(zhí)行下的可見性有序性
有序性簡(jiǎn)單來說就是程序代碼執(zhí)行的順序是否按照我們編寫代碼的順序執(zhí)行,一般來說,為了提高性能,編譯器和處理器會(huì)對(duì)指令做重排序,重排序分3類
編譯器優(yōu)化重排序,在不改變單線程程序語義的前提下,改變代碼的執(zhí)行順序
指令集并行的重排序,對(duì)于不存在數(shù)據(jù)依賴的指令,處理器可以改變語句對(duì)應(yīng)指令的執(zhí)行順序來充分利用CPU資源
內(nèi)存系統(tǒng)的重排序,也就是前面說的CPU的內(nèi)存亂序訪問問題3.
也就是說,我們編寫的源代碼到最終執(zhí)行的指令,會(huì)經(jīng)過三種重排序
有序性會(huì)帶來可見性問題,所以可以通過內(nèi)存屏障指令來進(jìn)制特定類型的處理器重排序從JMM層面解決線程并發(fā)問題
從硬件層面的分析了解到原子性、有序性、可見性的本質(zhì)以后,知道硬件層面針對(duì)這三個(gè)問題的解決辦法,原子性是通過總線鎖或緩存鎖來實(shí)現(xiàn),而有序性和可見性可以通過內(nèi)存屏障來解決。那么在軟件層面,如何解決原子性、有序性、可見性問題呢?答案就是: JMM(JavaMemoryModel)內(nèi)存模型
硬件層面的原子性、有序性、可見性在不同的CPU架構(gòu)和操作系統(tǒng)中的實(shí)現(xiàn)可能都不一樣,而Java語言的特性是 write once,run anywhere,意味著JVM層面需要屏蔽底層的差異,因此在JVM規(guī)范中定義了JMM。
JMM屬于語言級(jí)別的抽象內(nèi)存模型,可以簡(jiǎn)單理解為對(duì)硬件模型的抽象,它定義了共享內(nèi)存中多線程程序讀寫操作的行為規(guī)范,也就是在虛擬機(jī)中將共享變量存儲(chǔ)到內(nèi)存以及從內(nèi)存中取出共享變量的底層細(xì)節(jié)。
通過這些規(guī)則來規(guī)范對(duì)內(nèi)存的讀寫操作從而保證指令的正確性,它解決了CPU多級(jí)緩存、處理器優(yōu)化、指令重排序?qū)е碌膬?nèi)存訪問問題,保證了并發(fā)場(chǎng)景下的可見性。
需要注意的是,JMM并沒有限制執(zhí)行引擎使用處理器的寄存器或者高速緩存來提升指令執(zhí)行速度,也沒有限制編譯器對(duì)指令進(jìn)行重排序,也就是說在JMM中,也會(huì)存在緩存一致性問題和指令重排序問題。只是JMM把底層的問題抽象到JVM層面,再基于CPU層面提供的內(nèi)存屏障指令,以及限制編譯器的重排序來解決并發(fā)問題
Java內(nèi)存模型定義了線程和內(nèi)存的交互方式,在JMM抽象模型中,分為主內(nèi)存、工作內(nèi)存;主內(nèi)存是所有線程共享的,一般是實(shí)例對(duì)象、靜態(tài)字段、數(shù)組對(duì)象等存儲(chǔ)在堆內(nèi)存中的變量。工作內(nèi)存是每個(gè)線程獨(dú)占的,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,不能直接讀寫主內(nèi)存中的變量,線程之間的共享變量值的傳遞都是基于主內(nèi)存來完成。
在JMM中,定義了8個(gè)原子操作來實(shí)現(xiàn)一個(gè)共享變量如何從主內(nèi)存拷貝到工作內(nèi)存,以及如何從工作內(nèi)存同步到主內(nèi)存,交互如下
8個(gè)原子操作指令順序一致性
lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。
unlock(解鎖):作用于主內(nèi)存變量,把一個(gè)處于鎖定狀態(tà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è)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中。
如果要把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存,就需要按順尋地執(zhí)行read和load操作,如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要按順序地執(zhí)行store和write操作。JMM只要求這兩個(gè)操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行。也就是read和load之間,store和write之間是可以插入其他指令的,如對(duì)主內(nèi)存中的變量a、b進(jìn)行訪問時(shí),可能的順序是read a,read b,load b, load a。
JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致,因?yàn)槿绻胍WC執(zhí)行結(jié)果一致,意味著JMM需要進(jìn)制處理器和編譯器的優(yōu)化,這對(duì)于程序的執(zhí)行性能會(huì)產(chǎn)生很大的影響。所以在未同步程序的執(zhí)行中,由于執(zhí)行順序的不確定性導(dǎo)致結(jié)果無法預(yù)測(cè)。我們可以使用同步原語比如 synchronized,volatile、final來實(shí)現(xiàn)程序的同步操作來保證順序一致性
假如有兩個(gè)線程A和B并行執(zhí)行,A和B線程分別都有3個(gè)操作,在程序中的順序是 A1->A2->A3, B1->B2->B3。
假設(shè)這兩個(gè)程序沒有使用同步原語,那么線程并行執(zhí)行的效果可能是
如果這兩個(gè)程序使用了監(jiān)視器鎖來實(shí)現(xiàn)正確同步,那么執(zhí)行的過程一定是
重排序CPU層面的內(nèi)存亂序訪問屬于重排序的一部分,同時(shí)我們還提到了編譯器的優(yōu)化執(zhí)行的重排序。重排序是一種優(yōu)化手段,但是在多線程并發(fā)中,會(huì)導(dǎo)致可見性問題。
編譯器的重排序是指,在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序來優(yōu)化程序的性能.
編譯器的重排序和CPU的重排序的原則一樣,會(huì)遵守?cái)?shù)據(jù)依賴性原則,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序,比如下面的代碼,這三種情況在單線程里面如果改變代碼的執(zhí)行順序,都會(huì)導(dǎo)致結(jié)果不一致,所以重排序不會(huì)對(duì)這類的指令做優(yōu)化,也就是需要滿足 as-if-serial語義
//寫后讀 a=1; b=1; //寫后寫 a=1; a=2; //讀后寫 a=b; b=1;
as-if-serial語義
as-if-serial語義的意思是不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變,編譯器、處理器都必須遵守這個(gè)語義
JMM層面的內(nèi)存屏障
為了保證內(nèi)存可見性,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障來禁止特定類型的處理器的重排序,在JMM中把內(nèi)存屏障分為四類
屏障的作用這里就不重復(fù)再說了,實(shí)際上JMM層面的內(nèi)存屏障就是對(duì)CPU層面的內(nèi)存屏障指令做的包裝,作用是通過在合適的位置插入內(nèi)存屏障來保證可見性
JVM是如何在JMM層面解決原子性、有序性、可見性問題的呢?相信通過上面的分析,基本上有了答案
原子性:Java中提供了兩個(gè)高級(jí)指令 monitorenter和 monitorexit,也就是對(duì)應(yīng)的synchronized同步鎖來保證原子性
可見性:volatile、synchronized、final都可以解決可見性問題
有序性:synchronized和volatile可以保證多線程之間操作的有序性,volatile會(huì)禁止指令重排序
volatile源碼分析如果你看到這個(gè)章節(jié)了,意味著你對(duì)可見性有一個(gè)清晰的認(rèn)識(shí)了,也知道JMM是基于禁止指令重排序來實(shí)現(xiàn)可見性的,那么我們?cè)賮矸治鰒olatile的源碼,就會(huì)簡(jiǎn)單很多
基于最開始演示的這段代碼作為入口
public class VolatileDemo { public volatile static boolean stop=false; public static void main(String[] args) throws InterruptedException { Thread thread=new Thread(()->{ int i=0; while(!stop){ i++; } }); thread.start(); System.out.println("begin start thread"); Thread.sleep(1000); stop=true; } }
通過 javap-vVolatileDemo.class查看字節(jié)碼指令
public static volatile boolean stop; descriptor: Z flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE ...//省略 public static void main(java.lang.String[]) throws java.lang.InterruptedException; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=2, args_size=1 0: new #2 // class java/lang/Thread 3: dup 4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; 9: invokespecial #4 // Method java/lang/Thread."":(Ljava/lang/Runnable;)V 12: astore_1 13: aload_1 14: invokevirtual #5 // Method java/lang/Thread.start:()V 17: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 20: ldc #7 // String begin start thread 22: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 25: ldc2_w #9 // long 1000l 28: invokestatic #11 // Method java/lang/Thread.sleep:(J)V 31: iconst_1 32: putstatic #12 // Field stop:Z 35: return
注意被修飾了volatile關(guān)鍵字的 stop字段,會(huì)多一個(gè) ACC_VOLATILE的flag,在給 stop復(fù)制的時(shí)候,調(diào)用的字節(jié)碼是 putstatic,這個(gè)字節(jié)碼會(huì)通過BytecodeInterpreter解釋器來執(zhí)行,找到Hotspot的源碼 bytecodeInterpreter.cpp文件,搜索 putstatic指令定位到代碼
CASE(_putstatic): { u2 index = Bytes::get_native_u2(pc+1); ConstantPoolCacheEntry* cache = cp->entry_at(index); if (!cache->is_resolved((Bytecodes::Code)opcode)) { CALL_VM(InterpreterRuntime::resolve_get_put(THREAD, (Bytecodes::Code)opcode), handle_exception); cache = cp->entry_at(index); } #ifdef VM_JVMTI if (_jvmti_interp_events) { int *count_addr; oop obj; // Check to see if a field modification watch has been set // before we take the time to call into the VM. count_addr = (int *)JvmtiExport::get_field_modification_count_addr(); if ( *count_addr > 0 ) { if ((Bytecodes::Code)opcode == Bytecodes::_putstatic) { obj = (oop)NULL; } else { if (cache->is_long() || cache->is_double()) { obj = (oop) STACK_OBJECT(-3); } else { obj = (oop) STACK_OBJECT(-2); } VERIFY_OOP(obj); } CALL_VM(InterpreterRuntime::post_field_modification(THREAD, obj, cache, (jvalue *)STACK_SLOT(-1)), handle_exception); } } #endif /* VM_JVMTI */ // QQQ Need to make this as inlined as possible. Probably need to split all the bytecode cases // out so c++ compiler has a chance for constant prop to fold everything possible away. oop obj; int count; TosState tos_type = cache->flag_state(); count = -1; if (tos_type == ltos || tos_type == dtos) { --count; } if ((Bytecodes::Code)opcode == Bytecodes::_putstatic) { Klass* k = cache->f1_as_klass(); obj = k->java_mirror(); } else { --count; obj = (oop) STACK_OBJECT(count); CHECK_NULL(obj); } // // Now store the result // int field_offset = cache->f2_as_index(); if (cache->is_volatile()) { if (tos_type == itos) { obj->release_int_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == atos) { VERIFY_OOP(STACK_OBJECT(-1)); obj->release_obj_field_put(field_offset, STACK_OBJECT(-1)); OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0); } else if (tos_type == btos) { obj->release_byte_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ltos) { obj->release_long_field_put(field_offset, STACK_LONG(-1)); } else if (tos_type == ctos) { obj->release_char_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == stos) { obj->release_short_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ftos) { obj->release_float_field_put(field_offset, STACK_FLOAT(-1)); } else { obj->release_double_field_put(field_offset, STACK_DOUBLE(-1)); } OrderAccess::storeload(); } else { if (tos_type == itos) { obj->int_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == atos) { VERIFY_OOP(STACK_OBJECT(-1)); obj->obj_field_put(field_offset, STACK_OBJECT(-1)); OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0); } else if (tos_type == btos) { obj->byte_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ltos) { obj->long_field_put(field_offset, STACK_LONG(-1)); } else if (tos_type == ctos) { obj->char_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == stos) { obj->short_field_put(field_offset, STACK_INT(-1)); } else if (tos_type == ftos) { obj->float_field_put(field_offset, STACK_FLOAT(-1)); } else { obj->double_field_put(field_offset, STACK_DOUBLE(-1)); } } ...//省略很多代碼
其他代碼不用管,直接看 cache->is_volatile()這段代碼,cache是 stop在常量池緩存中的一個(gè)實(shí)例,這段代碼是判斷這個(gè)cache是否是被 volatile修飾, is_volatile()方法的定義在 accessFlags.hpp文件中,代碼如下
public: // Java access flags ...// bool is_volatile () const { return (_flags & JVM_ACC_VOLATILE ) != 0; } bool is_transient () const { return (_flags & JVM_ACC_TRANSIENT ) != 0; } bool is_native () const { return (_flags & JVM_ACC_NATIVE ) != 0; }
is_volatile是判斷是否有 ACC_VOLATILE這個(gè)flag,很顯然,通過 volatile修飾的stop的字節(jié)碼中是存在這個(gè)flag的,所以 is_volatile()返回true
接著,根據(jù)當(dāng)前字段的類型來給 stop賦值,執(zhí)行 release_byte_field_put方法賦值,這個(gè)方法的實(shí)現(xiàn)在 oop.inline.hpp中
inline void oopDesc::release_byte_field_put(int offset, jbyte contents) { OrderAccess::release_store(byte_field_addr(offset), contents); }
賦值的動(dòng)作被包裝了一層,看看 OrderAccess::release_store做了什么事情呢?這個(gè)方法的定義在 orderAccess.hpp中,具體的實(shí)現(xiàn),根據(jù)不同的操作系統(tǒng)和CPU架構(gòu),調(diào)用不同的實(shí)現(xiàn)
以 orderAccess_linux_x86.inline.hpp為例,找到 OrderAccess::release_store的實(shí)現(xiàn),代碼如下
inline void OrderAccess::release_store(volatile jbyte* p, jbyte v) { *p = v; }
可以看到其實(shí)Java的volatile操作,在JVM實(shí)現(xiàn)層面第一步是給予了C++的原語實(shí)現(xiàn)。c/c++中的volatile關(guān)鍵字,用來修飾變量,通常用于語言級(jí)別的 memory barrier。被volatile聲明的變量表示隨時(shí)可能發(fā)生變化,每次使用時(shí),都必須從變量i對(duì)應(yīng)的內(nèi)存地址讀取,編譯器對(duì)操作該變量的代碼不再進(jìn)行優(yōu)化
賦值操作完成以后,如果大家仔細(xì)看了前面putstatic的代碼,就會(huì)發(fā)現(xiàn)還會(huì)執(zhí)行一個(gè) OrderAccess::storeload();的代碼,這個(gè)代碼的實(shí)現(xiàn)是在 orderAccess_linux_x86.inline.hpp,它其實(shí)就是一個(gè)storeload內(nèi)存屏障,JVM層面的四種內(nèi)存屏障的定義以及實(shí)現(xiàn)
inline void OrderAccess::loadload() { acquire(); } inline void OrderAccess::storestore() { release(); } inline void OrderAccess::loadstore() { acquire(); } inline void OrderAccess::storeload() { fence(); }
當(dāng)調(diào)用 storeload屏障時(shí),它會(huì)調(diào)用fence()方法
inline void OrderAccess::fence() { if (os::is_MP()) { //返回是否多處理器,如果是多處理器才有必要增加內(nèi)存屏障 // always use locked addl since mfence is sometimes expensive #ifdef AMD64 //__asm__ volatile 嵌入?yún)R編指令 //lock 匯編指令,lock指令會(huì)鎖住操作的緩存行,也就是緩存鎖的實(shí)現(xiàn) __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); #else __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory"); #endif } }
os::is_MP()判斷是否是多核,如果是單核,那么就不存在內(nèi)存不可見或者亂序的問題 __volatile__:禁止編譯器對(duì)代碼進(jìn)行某些優(yōu)化.
Lock :匯編指令,lock指令會(huì)鎖住操作的緩存行(cacheline), 一般用于read-Modify-write的操作;用來保證后續(xù)的操作是原子的
cc代表的是寄存器,memory代表是內(nèi)存;這邊同時(shí)用了”cc”和”memory”,來通知編譯器內(nèi)存或者寄存器內(nèi)的內(nèi)容已經(jīng)發(fā)生了修改,要重新生成加載指令(不可以從緩存寄存器中取)
這邊的read/write請(qǐng)求不能越過lock指令進(jìn)行重排,那么所有帶有l(wèi)ock prefix指令(lock ,xchgl等)都會(huì)構(gòu)成一個(gè)天然的x86 Mfence(讀寫屏障),這里用lock指令作為內(nèi)存屏障,然后利用asm volatile("" ::: "cc,memory")作為編譯器屏障. 這里并沒有使用x86的內(nèi)存屏障指令(mfence,lfence,sfence),應(yīng)該是跟x86的架構(gòu)有關(guān)系,x86處理器是強(qiáng)一致內(nèi)存模型
storeload屏障是固定調(diào)用的方法?為什么要固定調(diào)用呢?
原因是:避免volatile寫與后面可能有的volatile讀/寫操作重排序。因?yàn)榫幾g器常常無法準(zhǔn)確判斷在一個(gè)volatile寫的后面是否需要插入一個(gè)StoreLoad屏障。為了保證能正確實(shí)現(xiàn)volatile的內(nèi)存語義,JMM在采取了保守策略:在每個(gè)volatile寫的后面,或者在每個(gè)volatile讀的前面插入一個(gè)StoreLoad屏障。因?yàn)関olatile寫-讀內(nèi)存語義的常見使用模式是:一個(gè)寫線程寫volatile變量,多個(gè)讀線程讀同一個(gè)volatile變量。當(dāng)讀線程的數(shù)量大大超過寫線程時(shí),選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執(zhí)行效率的提升。從這里可以看到JMM在實(shí)現(xiàn)上的一個(gè)特點(diǎn):首先確保正確性,然后再去追求執(zhí)行效率
總結(jié)綜上分析可以得知,volatile是通過防止指令重排序來實(shí)現(xiàn)多線程對(duì)于共享內(nèi)存的可見性。內(nèi)容涉及比較多,有問題可以微信留言
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/72572.html
摘要:原文地址游客前言金三銀四,很多同學(xué)心里大概都準(zhǔn)備著年后找工作或者跳槽。最近有很多同學(xué)都在交流群里求大廠面試題。 最近整理了一波面試題,包括安卓JAVA方面的,目前大廠還是以安卓源碼,算法,以及數(shù)據(jù)結(jié)構(gòu)為主,有一些中小型公司也會(huì)問到混合開發(fā)的知識(shí),至于我為什么傾向于混合開發(fā),我的一句話就是走上編程之路,將來你要學(xué)不僅僅是這些,豐富自己方能與世接軌,做好全棧的裝備。 原文地址:游客kutd...
摘要:如何在線程池中提交線程內(nèi)存模型相關(guān)問題什么是的內(nèi)存模型,中各個(gè)線程是怎么彼此看到對(duì)方的變量的請(qǐng)談?wù)動(dòng)惺裁刺攸c(diǎn),為什么它能保證變量對(duì)所有線程的可見性既然能夠保證線程間的變量可見性,是不是就意味著基于變量的運(yùn)算就是并發(fā)安全的請(qǐng)對(duì)比下對(duì)比的異同。 并發(fā)編程高級(jí)面試面試題 showImg(https://upload-images.jianshu.io/upload_images/133416...
摘要:我的是忙碌的一年,從年初備戰(zhàn)實(shí)習(xí)春招,年三十都在死磕源碼,三月份經(jīng)歷了阿里五次面試,四月順利收到實(shí)習(xí)。因?yàn)槲倚睦砗芮宄业哪繕?biāo)是阿里。所以在收到阿里之后的那晚,我重新規(guī)劃了接下來的學(xué)習(xí)計(jì)劃,將我的短期目標(biāo)更新成拿下阿里轉(zhuǎn)正。 我的2017是忙碌的一年,從年初備戰(zhàn)實(shí)習(xí)春招,年三十都在死磕JDK源碼,三月份經(jīng)歷了阿里五次面試,四月順利收到實(shí)習(xí)offer。然后五月懷著忐忑的心情開始了螞蟻金...
閱讀 3021·2021-11-23 09:51
閱讀 3623·2021-10-13 09:39
閱讀 2511·2021-09-22 15:06
閱讀 894·2019-08-30 15:55
閱讀 3164·2019-08-30 15:44
閱讀 1793·2019-08-30 14:05
閱讀 3448·2019-08-29 15:24
閱讀 2373·2019-08-29 12:44