Java併發機制的底層實現原理|8月更文挑戰

Java併發機制的底層實現原理

Java代碼在編譯後會變成Java字節碼,字節碼被類加載器加載到JVM裏,JVM執行字節碼,最終須要轉化爲彙編指令在CPU上執行,Java中所使用的併發機制依賴於JVM的實現和CPU的指令。編程

2.1 volatile的應用

在多線程併發編程中synchronized和volatile都扮演着重要的角色,volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的「可見性」。可見性的意思是當一個線程修改一個共享變量時,另一個線程能讀到這個修改的值。若是volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,由於它不會引發線程上下文的切換和調度。本文將深刻分析在硬件層面上Intel處理器是如何實現volatile的,經過深刻分析幫助咱們正確地使用volatile變量。數組

1.volatile的定義與實現原理

Java語言規範第3版中對volatile的定義以下:Java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該確保經過排他鎖單獨得到這個變量。Java語言提供了volatile,在某些狀況下比鎖要更加方便。若是一個字段被聲明成volatile,Java線程內存模型確保全部線程看到這個變量的值是一致的。緩存

volatile是如何來保證可見性的呢?安全

有volatile變量修飾的共享變量進行寫操做的時候會多出Lock前綴的指令,Lock前綴的指令在多核處理器下會引起了兩件事情markdown

1)將當前處理器緩存行的數據寫回到系統內存。多線程

2)這個寫回內存的操做會使在其餘CPU裏緩存了該內存地址的數據無效。併發

爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。若是對聲明瞭volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題。因此,在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀 態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。框架

volatile的兩條實現原則。編程語言

1)Lock前綴指令會引發處理器緩存回寫到內存。測試

Lock前綴指令致使在執行指令期間,聲言處理器的LOCK#信號。在多處理器環境中,LOCK#信號確保在聲言該信號期間,處理器能夠獨佔任何共享內存[。可是,在最近的處理器裏,**LOCK#信號通常不鎖總線,而是鎖緩存,畢竟鎖總線開銷的比較大。**在8.1.4節有詳細說明鎖定操做對處理器緩存的影響,對於Intel486和Pentium處理器,在鎖操做時,老是在總線上聲言LOCK#信號。但在P6和目前的處理器中,若是訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反,它會鎖定這塊內存區域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的原子性,此操做被稱爲「緩存鎖定」,緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據。

2)一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效。

處理器使用嗅探技術保證它的內部緩存、系統內存和其餘處理器的緩存的數據在總線上保持一致。例如,在Pentium和P6 family處理器中,若是經過嗅探一個處理器來檢測其餘處理器打算寫內存地址,而這個地址當前處於共享狀態,那麼正在嗅探的處理器將使它的緩存行無效,在下次訪問相同內存地址時,強制執行緩存行填充。

2.2 synchronized的實現原理與應用

具體表現爲如下3種形式。

  • 對於普通同步方法,鎖是當前實例對象。
  • 對於靜態同步方法,鎖是當前類的Class對象。
  • 對於同步方法塊,鎖是Synchonized括號裏配置的對象。

當一個線程試圖訪問同步代碼塊時,它首先必須獲得鎖,退出或拋出異常時必須釋放鎖。

那麼鎖到底存在哪裏呢?鎖裏面會存儲什麼信息呢?

從JVM規範中能夠看到Synchonized在JVM裏的實現原理,JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但二者的實現細節不同。代碼塊同步

是使用monitorenter和monitorexit指令實現的,而方法同步是使用另一種方式實現的,細節在JVM規範裏並無詳細說明。可是,方法的同步一樣可使用這兩個指令來實現。

monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每一個monitorenter必須有對應的monitorexit與之配對。**任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的全部權,即嘗試得到對象的鎖。

2.2.1 Java對象頭

synchronized用的鎖是存在Java對象頭裏的。若是對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。

Java對象頭裏的Mark Word裏默認存儲對象的HashCode、分代年齡和鎖標記位。

2.2.2 鎖的升級與對比

鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率。

偏向鎖

當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,

只需簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。若是測試成功,表示線程已經得到了鎖。若是測試失敗,則須要再測試一下Mark

Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):若是沒有設置,則使用CAS競爭鎖;若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

偏向鎖是如何撤銷的?

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有正在執行的字節碼)。它會首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否活着,若是線程不處於活動狀態,則將對象頭設置成無鎖狀態;若是線程仍然活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼從新偏向於其餘線程,要麼恢復到無鎖或者標記對象不適合做爲偏向鎖,最後喚醒暫停的線程。圖2-1中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。

image-20210603200102932

輕量級鎖

(1)輕量級鎖加鎖

線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並 將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。而後線程嘗試使用 CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖,若是失 敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

(2)輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操做將Displaced Mark Word替換回到對象頭,若是成 功,則表示沒有競爭發生。若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。圖2-2是 兩個線程同時爭奪鎖,致使鎖膨脹的流程圖。

2.3 原子操做的實現原理

處理器如何實現原子操做?

第一個機制是經過總線鎖保證原子性。

第二個機制是經過緩存鎖定來保證原子性。

可是有兩種狀況下處理器不會使用緩存鎖定。

第一種狀況是:當操做的數據不能被緩存在處理器內部,或操做的數據跨多個緩存行(cache line)時,則處理器會調用總線鎖定。

第二種狀況是:有些處理器不支持緩存鎖定。對於Intel 486和Pentium處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。

Java如何實現原子操做?

在Java中能夠經過鎖和循環CAS的方式來實現原子操做。

使用循環CAS實現原子操做

CAS實現原子操做的三大問題

1)ABA問題。

2)循環時間長開銷大。

3)只能保證一個共享變量的原子操做。

使用鎖機制實現原子操做

鎖機制保證了只有得到鎖的線程纔可以操做鎖定的內存區域。JVM內部實現了不少種鎖機制,有偏向鎖、輕量級鎖和互斥鎖。有意思的是除了偏向鎖,JVM實現鎖的方式都用了循環CAS,即當一個線程想進入同步塊的時候使用循環CAS的方式來獲取鎖,當它退出同步塊的時候使用循環CAS釋放鎖。

2.4 本章小結

本章咱們一塊兒研究了volatile、synchronized和原子操做的實現原理。Java中的大部分容器和框架都依賴於本章介紹的volatile和原子操做的實現原理,瞭解這些原理對咱們進行併發編程會更有幫助。

相關文章
相關標籤/搜索