天天看點

[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用

<a href="#volative%e7%9a%84%e5%ba%94%e7%94%a8">volative的應用</a>

<a href="#volatile%e7%9a%84%e5%ae%9a%e4%b9%89%e4%b8%8e%e5%ae%9e%e7%8e%b0%e5%8e%9f%e7%90%86">volatile的定義與實作原理</a>

<a href="#synchronized%e7%9a%84%e5%ae%9e%e7%8e%b0%e5%8e%9f%e7%90%86%e5%92%8c%e5%ba%94%e7%94%a8">synchronized的實作原理和應用</a>

<a href="#java%e5%af%b9%e8%b1%a1%e5%a4%b4">java對象頭</a>

<a href="#%e9%94%81%e5%8d%87%e7%ba%a7">鎖更新</a>

<a href="#%e5%81%8f%e5%90%91%e9%94%81">偏向鎖</a>

<a href="#%e5%81%8f%e5%90%91%e9%94%81%e7%9a%84%e6%92%a4%e9%94%80">偏向鎖的撤銷</a>

<a href="#%e5%85%b3%e9%97%ad%e5%81%8f%e5%90%91%e9%94%81">關閉偏向鎖</a>

<a href="#%e8%bd%bb%e9%87%8f%e9%94%81">輕量鎖</a>

<a href="#%e8%bd%bb%e9%87%8f%e9%94%81%e5%8a%a0%e9%94%81">輕量鎖加鎖</a>

<a href="#%e8%bd%bb%e9%87%8f%e9%94%81%e8%a7%a3%e9%94%81">輕量鎖解鎖</a>

<a href="#%e9%94%81%e7%9a%84%e4%bc%98%e7%bc%ba%e7%82%b9%e5%af%b9%e6%af%94">鎖的優缺點對比</a>

<a href="#%e5%8e%9f%e5%ad%90%e6%93%8d%e4%bd%9c%e7%9a%84%e5%ae%9e%e7%8e%b0%e5%8e%9f%e7%90%86">原子操作的實作原理</a>

<a href="#%e6%9c%af%e8%af%ad%e5%ae%9a%e4%b9%89">術語定義</a>

<a href="#%e5%a4%84%e7%90%86%e5%99%a8%e5%ae%9e%e7%8e%b0%e5%8e%9f%e5%ad%90%e6%93%8d%e4%bd%9c">處理器實作原子操作</a>

<a href="#%e4%bd%bf%e7%94%a8%e6%80%bb%e7%ba%bf%e9%94%81%e4%bf%9d%e8%af%81%e5%8e%9f%e5%ad%90%e6%80%a7">使用總線鎖保證原子性</a>

<a href="#%e4%bd%bf%e7%94%a8%e7%bc%93%e5%ad%98%e9%94%81%e4%bf%9d%e8%af%81%e5%8e%9f%e5%ad%90%e6%80%a7">使用緩存鎖保證原子性</a>

<a href="#java%e5%a6%82%e4%bd%95%e5%ae%9e%e7%8e%b0%e5%8e%9f%e5%ad%90%e6%93%8d%e4%bd%9c">java如何實作原子操作</a>

java程式設計語言允許線程通路共享變量,為了確定共享變量能被準确和一緻地更新,線程應該確定通過排他鎖單獨獲得這個變量。java語言提供了volatile,在某些情況下比鎖要更加友善。如果一個字段被聲明成volatile,java線程記憶體模型確定所有線程看到這個變量的值是一緻的。

術語

英文單詞

術語描述

記憶體屏障

memory barriers

一組處理器指令,用于實作對記憶體操作的順序限制

緩沖行

cache line

緩存中可以配置設定的最小存儲機關。

原子操作

atomic operations

不可中斷的一個或一系列操作

有volatile變量修飾的共享變量進行寫操作的時候會多出一些彙編代碼,加入lock字首。lock字首的指令在多核處理器會引發兩件事情

1. 将目前處理器緩存行的資料寫回到系統記憶體

2. 這個寫回記憶體的操作會使在其他cpu裡緩存了該記憶體位址的資料無效。

在多處理器下,為了保證各個處理器的緩存是一緻的,會實作緩存一緻性協定,每個處理器通過嗅探在總線上傳播的資料來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的記憶體位址被修改,就會将目前處理器的緩存行設定成無效狀态,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器緩存裡。

volatile的兩條實作原則

1. lock字首指令會引起處理器緩存回寫到記憶體

2. 一個處理器的緩存回寫到記憶體會導緻其他處理器的緩存無效

java中的每一個對象都可以作為鎖。具體表現為以下3種形式:

1. 對于普通同步方法,鎖是目前執行個體對象。

2. 對于靜态同步方法,鎖是目前類的class對象。

3. 對于同步方法塊,鎖是synchonized括号裡配置的對象。

jvm基于進入和退出monitor對象來實作方法同步和代碼塊同步,但兩者的實作細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實作的,而方法同步是使用另外一種方式實作的,細節在jvm規範裡并沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實作。

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

synchronized用的鎖是存在java對象頭裡的。如果對象是數組類型,則虛拟機用3個字寬

(word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛拟機中,1字寬等于4位元組,即32bit

[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用

java對象頭裡的mark word裡預設存儲對象的hashcode、分代年齡和鎖标記位。32位jvm

的mark word的預設存儲結構如下圖示

[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用

在運作期間,mark word裡存儲的資料會随着鎖标志位的變化而變化。mark word可能變

化為存儲以下4種資料,如下圖示

[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用

在64位虛拟機下,mark word是64bit大小的,如下圖示

[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用

java se 1.6中,鎖一共有4種狀态,級别從低到高依次是:

1. 無鎖狀态

2. 偏向鎖狀态

3. 輕量級鎖狀态

4. 重量級鎖狀态

這幾個狀态會随着競争情況逐漸更新。

大多數情況下,鎖不僅不存在多線程競争,而且總是由同

一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程通路同步塊并擷取鎖時,會在對象頭和棧幀中的鎖記錄裡存儲鎖偏向的線程id,以後該線程在進入和退出同步塊時不需要進行cas操作來加鎖和解鎖,隻需簡單地測試一下對象頭的mark word裡是否存儲着指向目前線程的偏向鎖。

偏向鎖使用了一種等到競争出現才釋放鎖的機制,是以當其他線程嘗試競争偏向鎖時,持有偏向鎖的線程才會釋放鎖。

當出現偏向鎖競争的時候,按如下步驟執行

1. 暫停擁有偏向鎖的線程

2. 檢查持有偏向鎖的線程是否還alive,若不是,則将對象頭設定為無鎖狀态,否則執行3

3. 線程仍然活着,執行偏向鎖的棧,周遊偏向對象的鎖記錄,棧中的鎖記錄和對象頭的mark word要麼重新偏向于其他線程,要麼回複到無鎖或者标記對象不适合作為偏向鎖

4. 喚醒暫停的線程

[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用

偏向鎖在java 6和java 7裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才激活,如

有必要可以使用jvm參數來關閉延遲:

如果你确定應用程式裡所有的鎖通常情況下處于競争狀态,可以通過jvm參數關閉偏向鎖:

那麼程式預設會進入輕量級鎖狀态。

線程在執行同步塊之前,jvm會先在目前線程的棧桢中建立用于存儲鎖記錄的空間,并将對象頭中的mark word複制到鎖記錄中,官方稱為displaced mark word。然後線程嘗試使用cas将對象頭中的mark word替換為指向鎖記錄的指針。如果成功,目前線程獲得鎖,如果失 敗,表示其他線程競争鎖,目前線程便嘗試使用自旋來擷取鎖。

displaced mark word是整個輕量級鎖實作的關鍵,在cas中的compare就需要用它作為條件。在拷貝完object mark word之後,jvm做了一步交換指針的操作将object mark word裡的輕量級鎖指針指向lock record所在的stack指針,作用是讓其他線程知道,該object monitor已被占用(就像偏向鎖中用cas的方式将mark word的id指向目前嘗試擷取鎖的線程id,這裡是将mark word中的輕量級鎖指針以cas的方式嘗試指向目前線程的lock record,這樣别的線程便知道目前輕量鎖已經指向别的線程了)。lock record裡的owner指針指向object mark word的作用是為了在接下裡的運作過程中,識别哪個對象被鎖住了。

[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用
[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用

輕量級解鎖時,會使用原子的cas操作将displaced mark word替換回到對象頭,如果成

功,則表示沒有競争發生。如果失敗,表示目前鎖存在競争,鎖就會膨脹成重量級鎖。

輕量鎖的膨脹流程如下圖示

[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用
[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用

是以由輕量鎖切換到重量鎖,是發生在輕量鎖釋放鎖的期間,之前在擷取鎖的時候它拷貝了鎖對象頭的mark word,在釋放鎖的時候如果它發現在它持有鎖的期間有其他線程來嘗試擷取鎖了,并且該線程對mark word做了修改,兩者比對發現不一緻,則切換到重量鎖。

因為重量級鎖被修改了,所有display mark word和原來的mark word不一樣了。

因為自旋會消耗cpu,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖更新成重量級鎖,就不會再恢複到輕量級鎖狀态。
[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用

術語名稱

英文

解釋

緩存行

緩存的最小操作機關

比較并交換

compare and swap

cas操作需要輸入兩個數值,一個舊值(期望操作前的值)和一個新值,在操作期間先比較舊值有沒有發生變化,如果沒有,這換成新值,否則不交換

cpu流水線

cpu pipeline

記憶體順序沖突

memory order violation

記憶體順序沖突一般是由假共享引起的,假共享是指對個cpu同時修改同一個緩存行的不同部分而引起的其中一個cpu的操作無效,當出現這個記憶體順序沖突時,cpu必須清空流水線

如果多個處理器同時對共享變量進行讀改寫操作

(i++就是經典的讀改寫操作),那麼共享變量就會被多個處理器同時進行操作,這樣讀改寫操

作就不是原子的,操作完之後共享變量的值會和期望的不一緻。

[jjzhu學java之多線程筆記]java并發機制的底層實作原理volative的應用

所謂總線鎖就是使用處理器提供的一個

lock#信号,當一個處理器在總線上輸出此信号時,其他處理器的請求将被阻塞住,那麼該

處理器可以獨占共享記憶體。

第二個機制是通過緩存鎖定來保證原子性。在同一時刻,我們隻需保證對某個記憶體位址

的操作是原子性即可,但總線鎖定把cpu和記憶體之間的通信鎖住了,這使得鎖定期間,其他處

理器不能操作其他記憶體位址的資料,是以總線鎖定的開銷比較大,目前處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。

所謂“緩存鎖定”是指記憶體區域如果被緩存在處理器的緩存

行中,并且在lock操作期間被鎖定,那麼當它執行鎖操作回寫到記憶體時,處理器不在總線上聲言lock#信号,而是修改内部的記憶體位址,并允許它的緩存一緻性機制來保證操作的原子性,因為緩存一緻性機制會阻止同時修改由兩個以上處理器緩存的記憶體區域資料,當其他處理器回寫已被鎖定的緩存行的資料時,會使緩存行無效

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

1. 當操作的資料不能被緩存在處理器内部,或操作的資料跨多個緩存行

(cache line)時,則處理器會調用總線鎖定

2. 有些處理器不支援緩存鎖定。對于intel 486和pentium處理器,就算鎖定的

記憶體區域在處理器的緩存行中也會調用總線鎖定

在java中可以通過鎖和循環cas的方式來實作原子操作

1. 使用循環cas實作原子操作

2. cas實作原子操作的三大問題

- aba問題

因為cas需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化 則更新,但是如果一個值原來是a,變成了b,又變成了a,那麼使用cas進行檢查時會發現它 的值沒有發生變化,但是實際上卻變化了。aba問題的解決思路就是使用版本号。在變量前面 追加上版本号,每次變量更新的時候把版本号加1,那麼a→b→a就會變成1a→2b→3a。從 java 1.5開始,jdk的atomic包裡提供了一個類atomicstampedreference來解決aba問題 - 循環時間長開銷大 自旋cas如果長時間不成功,會給cpu帶來非常大的執行開銷。 - 隻能保證一個共享變量的原子操作 當對一個共享變量執行操作時,我們可以使用循 環cas的方式來保證原子操作,但是對多個共享變量操作時,循環cas就無法保證操作的原子性,這個時候就可以用鎖。還有一個取巧的辦法,就是把多個共享變量合并成一個共享變量來操作。 3. 使用鎖機制實作原子操作 鎖機制保證了隻有獲得鎖的線程才能夠操作鎖定的記憶體區域。jvm内部實作了很多種鎖 機制,有偏向鎖、輕量級鎖和互斥鎖。有意思的是除了偏向鎖,jvm實作鎖的方式都用了循環cas,即當一個線程想進入同步塊的時候使用循環cas的方式來擷取鎖,當它退出同步塊的時候使用循環cas釋放鎖