2-Java并發(fā)機制的底層實現(xiàn)原理

Java代碼在編譯后會變成Java字節(jié)碼,字節(jié)碼被類加載到JVM里,JVM執(zhí)行字節(jié)碼,最終需要轉(zhuǎn)化為匯編指令在CPU上執(zhí)行,Java中所使用的并發(fā)機制依賴于JVM的實現(xiàn)和CPU的指令。

1.CPU相關(guān)術(shù)語

相關(guān)CPU術(shù)語定義

2.volatile的應(yīng)用

volatile是輕量級的synchronized,它在多處理器開發(fā)中保證了共享變量的“可見性”。

可見性:是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。

如果volatile變量修飾符使用恰當?shù)脑?,它比synchronized的使用和執(zhí)行成本更低,因為它不會引起線程上下文的切換和調(diào)度。

①volatile的定義與實現(xiàn)原理

Java語言規(guī)范第3版中對volatile的定義:Java編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致地更新,線程應(yīng)該確保通過排他鎖單獨獲得這個變量Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個字段被聲明成volatile,Java線程內(nèi)存模型確保所有線程看到這個變量的值是一致的。

通過編譯器生成匯編指令來查看volatile進行寫操作時,CPU會做什么事情。

java代碼:

instance = new Singleton(); //instance是volatile變量

轉(zhuǎn)變成匯編代碼如下:

0x01a3deld: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

有volatile修飾的共享變量進行寫操作的時候會多出第二行匯編代碼。Lock前綴的指令在多核處理器下會引發(fā)兩件事情:

1)將當前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。

2)這個寫回內(nèi)存的操作會使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效。

為了提高處理速度,處理器不直接和內(nèi)存進行通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存(L1,L2或其他)后再進行操作,但操作完不知道何時會寫到內(nèi)存。如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發(fā)送一條Lock前綴的指令,將這個變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。

處理器使用嗅探技術(shù)保證它的內(nèi)部緩存、系統(tǒng)內(nèi)存和其他處理器的緩存的數(shù)據(jù)在總線上保持一致。如果通過嗅探一個處理器來檢測其他處理器打算寫內(nèi)存地址,而這個地址處于共享狀態(tài),那么正在嗅探的處理器將使它的緩存行無效,在下次訪問相同內(nèi)存地址時,強制執(zhí)行緩存行填充。

3.synchronized的實現(xiàn)原理與應(yīng)用

synchronized實現(xiàn)同步的基礎(chǔ):Java中的每一個對象都可以作為鎖。具體表現(xiàn)為以下3種形式:

  • 對于普通同步方法,鎖是當前實例對象。

  • 對于靜態(tài)同步方法,鎖是當前類的Class對象。

  • 對于同步方法塊,鎖是synchronized括號里配置的對象。

JVM基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步,但兩者的實現(xiàn)細節(jié)不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現(xiàn)的,方法同步是使用另外一種方式實現(xiàn)的,細節(jié)在JVM規(guī)范里并沒有詳細說明。

monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結(jié)束處和異常處,JVM要保證每個monitorenter必須有對應(yīng)的monitorexit與之配對。任何對象都有一個monitor與之關(guān)聯(lián),當且一個monitor被持有后,它將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時,將會嘗試獲取對象所對應(yīng)的monitor的所有權(quán),即嘗試獲得對象的鎖。

①Java對象頭

synchronized用的鎖是存在Java對象頭里的。如果對象是數(shù)組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象是非數(shù)組類型,則用2字寬存儲對象頭。32位虛擬機中,1字寬=4字節(jié)=32bit。


32位JVM的Mark Word的默認存儲結(jié)構(gòu)

運行期間,Mark Word里存儲的數(shù)據(jù)會隨著鎖標志位的變化而變化。


64位JVM的Mark Word的默認存儲結(jié)構(gòu)

②鎖的升級與對比

Java SE 1.6引入了“偏向鎖”和“輕量級鎖”,鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)、重量級鎖狀態(tài),這幾個狀態(tài)會隨著競爭情況逐漸升級。鎖可以升級但不能降級,目的是為了提高獲得鎖和釋放鎖的效率。

1)偏向鎖

HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。

當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖。

a.偏向鎖的撤銷

偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。

b.關(guān)閉偏向鎖

偏向鎖在Java6和7里是默認啟用的,但它在應(yīng)用程序啟動幾秒之后才激活,如有必要,可以使用JVM參數(shù)關(guān)閉延遲: -XX:BiasedLockingStartupDelay=0。 當確定程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:UseBiasedLocking=false ,那么程序默認會進入輕量級鎖狀態(tài)。

2)輕量級鎖

a.輕量級鎖加鎖

線程在執(zhí)行同步塊之前,JVM會先在當前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

b.輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態(tài)。當鎖處于這個狀態(tài)下,其他線程視圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。

3)鎖的優(yōu)缺點對比

4.原子操作的實現(xiàn)原理

①處理器如何實現(xiàn)原子操作

? 32位IA-32處理器使用基于對緩存加鎖或總線加鎖的方式來實現(xiàn)多處理器之間的原子操作。首先處理器會自動保證基本的內(nèi)存操作的原子性。處理器保證從系統(tǒng)內(nèi)存中讀取或者寫入一個字節(jié)是原子的,意思是當一個處理器讀取一個字節(jié)時,其他處理器不能訪問這個字節(jié)的內(nèi)存地址。Pentium6和最新的處理器能自動保證單處理器對同一個緩存行里進行16/32/64位的操作是原子的,但是復雜的內(nèi)存操作處理器是不能自動保證其原子性的,比如跨總線寬度、跨多個緩存行和跨頁表的訪問。但是處理器提供總線鎖定和緩存鎖定兩個機制來保證復雜內(nèi)存操作的原子性。

? 例子:i++

? 如果多個處理器同時對共享變量進行讀改寫操作(i++是經(jīng)典的讀改寫操作),那么共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的。

? 多個處理器同時從各自的緩存中讀取變量i,分別進行加1操作,然后分別寫入系統(tǒng)內(nèi)存中。那么想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操作緩存了該共享變量內(nèi)存地址的緩存。處理器使用總線鎖就是來解決這個問題的。

1)使用總線鎖保證原子性

? 所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占共享內(nèi)存。

2)使用緩存鎖保證原子性

? 在同一時刻,我們只需保證對某個內(nèi)存地址的操作是原子性即可,但總線鎖定把CPU和內(nèi)存之間的通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內(nèi)存地址的數(shù)據(jù),所以總線鎖定的開銷比較大,目前處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優(yōu)化。

? 所謂緩存鎖定是指內(nèi)存區(qū)域如果被緩存在處理器的緩存行中,并且在Lock操作期間被鎖定,那么當它執(zhí)行鎖操作回寫到內(nèi)存時,處理器不在總線上聲言LOCK#信號,而是修改內(nèi)部的內(nèi)存地址,并允許它的緩存一致性機制來保證操作的原子性。

? 緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內(nèi)存區(qū)域數(shù)據(jù),當其他處理器回寫已被鎖定的緩存行的數(shù)據(jù)時,會使緩存行無效。

有兩種情況下處理器不會使用緩存鎖定

a.當操作的數(shù)據(jù)不能被緩存在處理器內(nèi)部,或操作的數(shù)據(jù)跨多個緩存行(cache line)時,則處理器會調(diào)用總線鎖定。

b.有些處理器不支持緩存鎖定。如Intel 486和Pentium處理器。

②Java如何實現(xiàn)原子操作

在Java中可以通過鎖和循環(huán)CAS的方式來實現(xiàn)原子操作。

1)使用循環(huán)CAS實現(xiàn)原子操作

? JVM中的CAS操作正是利用了處理器提供的CMPXCHG指令實現(xiàn)的。自旋CAS實現(xiàn)的基本思路就是循環(huán)進行CAS操作直到成功為止。

? Java1.5 開始,JDK的并發(fā)包里提供了一些類來支持原子操作,如AtomicBoolean、AtomicInteger、AtomicLong。這些原子包裝類還提供了有用的工具方法,比如以原子的方式將當前值自增1和自減1。

? 以下代碼實現(xiàn)了一個基于CAS線程安全的計數(shù)器方法safeCount和一個非線程安全的計數(shù)器count:


2)CAS實現(xiàn)原子操作的三大問題

? ABA問題,循環(huán)時間長開銷大,以及只能保證一個共享變量的原子操作。

? CAS算法:一個CAS方法包含三個參數(shù)CAS(V,E,N)。V表示要更新的變量,E表示預期的值,N表示新值。只有當V的值等于E時,才會將V的值修改為N。如果V的值不等于E,說明已經(jīng)被其他線程修改了,當前線程可以放棄此操作,也可以再次嘗試次操作直至修改成功。基于這樣的算法,CAS操作即使沒有鎖,也可以發(fā)現(xiàn)其他線程對當前線程的干擾(臨界區(qū)值的修改),并進行恰當?shù)奶幚怼?/em>

  • ABA問題。因為CAS需要在操作值的時候,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化,但實際上卻變化了。

    ABA問題的解決思路就是使用版本號,在變量前面追加上版本號,每次變量更新的時候把版本號加1,那么A→B→A就會變成1A→2B箭頭3A。1.5開始JDK的Atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等于預期引用,并且檢查當前標志是否等于預期標志,全部相等,則以原子方式將該引用和該標志位的值設(shè)置為給定的更新值。

  • 循環(huán)時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執(zhí)行開銷。

  • 只能保證一個共享變量的原子操作。多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候可以用鎖。還有一個取巧的辦法,就是把多個變量合并成一個共享變量來操作,如i=2,j=a,合并一下ij=2a,然后用CAS操作ij。1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象里來進行CAS操作。

3)使用鎖機制實現(xiàn)原子操作

鎖機制保證了只有獲得鎖的線程才能操作鎖定的內(nèi)存區(qū)域。有意思的是除了偏向鎖,JVM實現(xiàn)鎖的方式都用了循環(huán)CAS,即當一個線程想進入同步塊的時候使用CAS的方式來獲取鎖,當它退出同步塊的時候使用循環(huán)CAS釋放鎖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容