Java 併發機制底層實現 —— volatile 原理、synchronize 鎖優化機制


本書部分摘自《Java 併發編程的藝術》編程


概述

相信你們都很熟悉如何使用 Java 編寫處理併發的代碼,也知道 Java 代碼在編譯後變成 Class 字節碼,字節碼被類加載器加載到 JVM 裏,JVM 執行字節碼,最終須要轉化爲彙編指令在 CPU 上執行。所以,Java 中所使用的併發機制實際上是依賴於 JVM 的實現和 CPU 的指令,因此瞭解 Java 併發機制的底層實現原理也是頗有必要的緩存


volatile 的應用

volatile 在多處理器開發中保證了共享變量的可見性。可見性的意思是當一個線程修改一個共享變量時,另一個線程能當即讀取到修改事後的值安全

1. volatile 的定義

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

2. volatile 的實現原理

在 Java 中咱們能夠直接使用 volatile 關鍵字,但它的底層是怎麼實現的呢?被 volatile 變量修飾的共享變量進行寫操做的時候會多生成一行彙編代碼,這行代碼使用了 Lock 指令。Lock 指令在多核處理器下會引起兩件事情:併發

  • 將當前處理器緩存行的數據寫回到系統內存
  • 這個寫回內存的操做會使在其餘 CPU 裏緩存了該內存地址的數據無效

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


synchronized 的應用

在多線程併發編程中 synchronized 一直是元老級角色,不少人稱呼它爲重量級鎖。不過,隨着 JavaSE 1.6 對 synchronized 進行了各類優化以後,有些狀況下它就並不那麼重了性能

Java 中的每個對象均可以做爲鎖,具體表現爲如下三種形式:測試

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

1. synchronized 原理

JVM 基於進入和退出 Monitor 對象來實現方法同步和代碼塊同步,但二者的實現細節不同。代碼塊同步是使用 monitorenter 和 monitorexit 指令實現,而方法同步是使用另一種方式實現,細節在 JVM 規範裏並無詳細說明,但方法的同步一樣可使用這兩個指令來實現優化

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

2. 鎖的升級

JavaSE 1.6 爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖,所以在 JavaSE 1.6 中,鎖一共有四種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。這幾個狀態會隨着競爭狀況逐漸升級,鎖能夠升級但不能降級,這是爲了提升得到鎖和釋放鎖的效率

3. 偏向鎖

研究發現,大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到。偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程,若是在運行過程當中,只有一個線程訪問,不存在多線程爭用的狀況,就會給線程加一個偏向鎖,線程不須要觸發同步就能得到鎖,下降得到鎖的代價

  • 偏向鎖的獲取

    當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程 ID,之後該線程在進入和退出同步塊時不須要進行 CAS 操做來加鎖和解鎖,只需測試一下對象頭的 Mark Word 是否存儲着指向當前線程的偏向鎖。若是測試成功,表示線程已經得到鎖,不然再測試一下 Mark Work 中偏向鎖的標識是否設置成 1(表示當前是偏向鎖),若是沒有設置,使用 CAS 競爭鎖,不然嘗試使用 CAS 將對象頭的偏向鎖指向當前線程

  • 偏向鎖的撤銷

    偏向鎖只有遇到其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行)。它首先會暫停擁有偏向鎖的線程,判斷持有偏向鎖的線程是否活動,若是線程不處於活動狀態,則將對象頭設置成無鎖狀態;若是對象仍然活着,撤銷偏向鎖後恢復到未鎖定或輕量級鎖的狀態

  • 關閉偏向鎖

    偏向鎖在 Java6 和 Java7 裏是默認開啓的,可是它在應用程序啓動幾秒以後才激活,若有必要可使用 JVM 參數來關閉延遲:-XX:BiasedLockingStartupDelay = 0。若是你肯定應用程序裏全部的鎖一般狀況下處於競爭狀態,能夠經過 JVM 參數來關閉偏向鎖:-XX:-UseBiasedLocking = false,那麼程序默認會進入輕量級鎖狀態

下圖是偏向鎖的得到和撤銷流程

4. 輕量級鎖

傳統的重量級鎖性能每每不如人意,由於 monitorenter 與 monitorexit 這兩個控制多線程同步的 bytecode 原語,是 JVM 依賴操做系統的互斥量來實現的。互斥是一種會致使線程掛起,並在較短的時間內又須要從新調度回原線程的,較爲消耗資源的操做,爲了優化性能,從 Java6 開始引入了輕量級鎖的概念。輕量級鎖本意是爲了減小多線程進入互斥的概率,並非要替代互斥,它利用了 CPU 原語 Compare-And-Swap(CAS),嘗試在進入互斥前,進行補救

  • 輕量級鎖加鎖

    線程在執行同步塊以前,JVM 先在當前線程的棧幀中建立用於存儲鎖記錄的空間,並將對象頭中的 Mark Word 複製到鎖記錄中,官方稱爲 Displaced Mark Word

    而後線程嘗試使用 CAS 將對象頭中的 Mark Word 替換爲指向鎖記錄的指針,若是成功,當前線程得到鎖,不然表示其餘線程競爭鎖,當前線程嘗試使用自旋來獲取鎖

  • 輕量級鎖解鎖

    輕量級鎖解鎖時,線程會使用原子的 CAS 操做將 Dispatch Mark Word 替換回到對象頭,若是成功,表示沒有競爭發生;若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖

下圖是輕量級鎖及膨脹流程圖

由於自旋會消耗 CPU,爲了不無用的自旋,一旦鎖升級爲重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其餘線程試圖獲取鎖時,都會被阻塞,當持有鎖的線程釋放鎖以後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭

5. 鎖的優缺點對比

優勢 缺點 適用場景
偏向鎖 加鎖和解鎖不須要額外的消耗,和執行非同步方法相比僅存在納秒級的差距 若是線程間存在鎖競爭,會帶來額外的鎖賒銷的消耗 適用於只有一個線程訪問同步塊場景
輕量級鎖 競爭的線程不會阻塞,提升了程序的響應速度 若是始終得不到鎖競爭的線程,使用自旋會消耗 CPU 追求響應時間,同步塊執行速度很是快
重量級鎖 線程競爭不使用自旋,不會消耗 CPU 線程阻塞,響應時間緩慢 追求吞吐量,同步塊執行速度較長
相關文章
相關標籤/搜索