多線程系列 - 基礎篇02 - 同步的原理 及 鎖在應用層的優化思路 60%

轉載自併發編程網 – ifeve.com本文連接地址: 聊聊併發(二)Java SE1.6中的Synchronizedjava

1.synchronized實現同步的基礎:

①:  對於普通方法,鎖是當前實例對象;編程

②:對於靜態同步方法,鎖是class對象。數組

③:同步方法塊,鎖是synchronized後面括號裏的對象。安全

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

  那麼鎖存在哪裏呢?併發

  鎖裏面會存儲什麼信息呢?性能

 

2.同步的原理

JVM規範規定JVM基於進入和退出Monitor對象來實現方法同步代碼塊同步,但二者的實現細節不同。測試

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

 monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處。spa

 JVM要保證每一個monitorenter必須有對應的monitorexit與之配對。

 任何對象都有一個 monitor 與之關聯,當且一個monitor 被持有後,它將處於鎖定狀態

④ 線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 monitor 的全部權,即嘗試得到對象的鎖

 

2.1JAVA對象頭

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

在32位虛擬機中,一字寬等於四字節,即32bit。

長度 內容 說明
32/64bit Mark Word 存儲對象的hashCode或鎖信息等。
32/64bit Class Metadata Address 存儲到對象類型數據的指針
32/64bit Array length 數組的長度(若是當前對象是數組)

Java對象頭裏的Mark Word裏默認存儲對象的HashCode,分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構以下:

  25 bit 4bit 1bit是不是偏向鎖 2bit鎖標誌位
無鎖狀態 對象的hashCode 對象分代年齡 0 01

在運行期間Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。Mark Word可能變化爲存儲如下4種數據:

鎖狀態

25 bit

4bit

1bit 2bit
23bit 2bit 是不是偏向鎖 鎖標誌位
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖 指向互斥量(重量級鎖)的指針 10
GC標記 11
偏向鎖 線程ID Epoch 對象分代年齡 1 01

在64位虛擬機下,Mark Word是64bit大小的,其存儲結構以下:

鎖狀態

25bit

31bit

1bit

4bit

1bit 2bit
    cms_free 分代年齡 偏向鎖 鎖標誌位
無鎖 unused hashCode     0 01
偏向鎖 ThreadID(54bit) Epoch(2bit)     1 01

 

2.2鎖的升級

Java SE1.6爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,

因此在Java SE1.6裏鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭狀況逐漸升級。

鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。

這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率,下文會詳細分析。

 

2.3偏向鎖

Hotspot的做者通過以往的研究發現大多數狀況下鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低而引入了偏向鎖。

   1)當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,

   2)之後該線程在進入和退出同步塊時不須要花費CAS操做來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖,

   3.1)若是測試成功,表示線程已經得到了鎖;

   3.2)若是測試失敗,則須要再測試下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖),

                     3.2.1) 若是沒有設置,則使用CAS競爭鎖;

                     3.2.2) 若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

 

偏向鎖的撤銷:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖

偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),

  它會首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否活着,

    若是線程不處於活動狀態,則將對象頭設置成無鎖狀態,

    若是線程仍然活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,

         棧中的鎖記錄和對象頭的Mark Word要麼從新偏向於其餘線程,

         要麼恢復到無鎖或者標記對象不適合做爲偏向鎖,最後喚醒暫停的線程。

下圖中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。

 

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

 

2.4輕量級鎖

輕量級鎖加鎖

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

而後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。

     若是成功,當前線程得到鎖,

     若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

輕量級鎖解鎖

 輕量級解鎖時,會使用原子的CAS操做來將Displaced Mark Word替換回到對象頭,

      若是成功,則表示沒有競爭發生。

      若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖是兩個線程同時爭奪鎖,致使鎖膨脹的流程圖。

由於自旋會消耗CPU,爲了不無用的自旋(好比得到鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。

當鎖處於這個狀態下,其餘線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖以後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。

 

3.鎖的優缺點對比

優勢

缺點

適用場景

偏向鎖

加鎖和解鎖不須要額外的消耗,和執行非同步方法比僅存在納秒級的差距。

若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。

適用於只有一個線程訪問同步塊場景。

輕量級鎖

競爭的線程不會阻塞,提升了程序的響應速度。

若是始終得不到鎖競爭的線程使用自旋會消耗CPU。

追求響應時間。

同步塊執行速度很是快。

重量級鎖

線程競爭不使用自旋,不會消耗CPU。

線程阻塞,響應時間緩慢。

追求吞吐量。

同步塊執行速度較長。

 

4.鎖在應用層的優化思路

4.1減小鎖持有的時間

  TODO


4.2減少鎖的粒度

ConcurrentHashMap.java


4.3鎖分離

BlockingQueue.java


4.4鎖粗化

 

5.參考源碼

本文一些內容參考了Hotspot源碼 。對象頭源碼markOop.hpp。偏向鎖源碼biasedLocking.cpp。以及其餘源碼ObjectMonitor.cpp和BasicLock.cpp。

相關文章
相關標籤/搜索