多線程知識梳理(3) synchronized 三部曲之鎖優化

1、前言

多線程知識梳理(2) - synchronized 基本使用 中,咱們介紹了使用重量鎖來實現的synchronized。今天,咱們就來一塊兒學習一下在JDK 1.6以後,對synchronized所採起的一系列優化措施。html

2、對象頭 & Monitor Record

在介紹優化方法以前,咱們須要介紹兩個重要的概念Java對象頭和Monitor編程

2.1 對象頭

Java&Android 基礎知識梳理(3) - 內存區域 中介紹內存區域的時候,對於一個Java對象所佔的內存區域是這麼介紹的: 安全

在運行過程當中,對象頭所包含數據的含義不是固定不變的,隨着 鎖狀態標誌位(下圖中紅框的範圍)的改變,其它字段所表示的含義也不一樣,以 32位的虛擬機爲例,下圖就是鎖狀態標誌位所對應的數據結構含義:

2.2 Monitor Record

Monitor是線程私有的數據結構,因爲一個線程可能進入多個不一樣的同步方法,這些方法有可能會關聯到不一樣的Monitor,所以每個線程都有一個可用的Monitor列表,同時還有一個全局的可用列表,Monitor數據結構包括如下成員變量:數據結構

  • Owner:初始時爲空表示當前沒有任何線程擁有該Monitor,當線程成功擁有該鎖後保存線程惟一標識,當鎖被釋放時又設置爲空。
  • EntryQ:關聯一個系統互斥鎖,阻塞全部試圖得到Monitor可是最終失敗了的線程。
  • RcThis:表示blockedwaiting在該Monitor上的全部線程的個數。
  • Nest:用來實現重入鎖的計數。
  • HashCode:保存從對象頭拷貝過來的HashCode值。
  • Candidate:用來避免沒必要要的阻塞或等待線程喚醒,由於每一次只有一個線程可以成功擁有鎖,若是每次前一個釋放鎖的線程喚醒全部正在阻塞或等待的線程,會引發沒必要要的上下文切換(從阻塞到就緒而後由於競爭鎖失敗又被阻塞)從而致使性能嚴重降低。Candidate只有兩種可能的值:0表示沒有須要喚醒的線程,1表示要喚醒一個繼任線程來競爭鎖。

3、實現優化

JDK 1.6以後,它對於鎖進行了一系列的優化措施,主要包括:自適應自旋鎖、鎖消除和鎖粗化。多線程

3.1 自旋鎖

因爲線程的阻塞和喚醒須要CPU從用戶態轉換成核心態,而頻繁的阻塞和喚醒對CPU來講是一件負擔很重的工做。併發

所以,咱們在發現鎖已經被其它線程佔有時,並不直接讓當前線程進入阻塞狀態,而是讓線程執行一段無心義的循環,待循環結束後,如何仍然沒法獲取到鎖,那麼才進入阻塞狀態。性能

決定自旋鎖性能的關鍵在於自旋次數的選擇,在JDK 1.6以後,引入了自適應自旋鎖,它會根據前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定新的自旋次數。學習

3.2 鎖消除

JVM檢測到不可能存在共享數據競爭,會對同步鎖進行鎖消除。優化

3.3 鎖粗化

在使用同步鎖的時候,須要讓同步塊的做用範圍儘量地小,僅在共享數據的實際做用域中才進行同步,這樣作的目的是爲了使須要同步的操做數量儘量縮小,若是存在鎖競爭,那麼等待鎖的線程也能儘快拿到鎖。操作系統

然而,若是一系列連續加鎖解鎖操做,可能會致使沒必要要的性能損耗,因此有時能夠將多個連續的加鎖、解鎖操做鏈接在一塊兒,擴展成一個範圍更大的鎖。

4、狀態優化

JDK 1.6以前,鎖只有兩種狀態:無鎖狀態和重量級鎖狀態,而在這以後增長爲四種狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,這種改進基於兩點考慮:

  • 無鎖狀態和重量級鎖狀態之間的切換是依賴於底層操做系統的Mutex Lock實現,操做系統實現線程之間的切換須要從用戶態切換到內核態,切換成本很高。
  • 實驗研究發現,對於絕大部分的鎖,在整個生命週期內都是不存在競爭的。

須要注意,對於鎖的這四種狀態,它們會隨着競爭的激烈而逐漸升級,可是它只容許鎖升級,不容許鎖降級。

無鎖狀態和重量級鎖狀態都比較好理解,下面咱們主要介紹新增的兩種鎖狀態:偏向鎖狀態輕量級鎖狀態

整個轉換的流程圖以下所示,在後面的介紹中能夠參考:

4.1 偏向鎖狀態

引入偏向鎖的目的是:在無多線程競爭的狀況下,儘可能減小沒必要要的輕量級鎖執行路徑,它的理想狀況下是在無競爭時把整個同步都去掉,連CAS操做都省略。

偏向鎖的意思是這個鎖會偏向於第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其它線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。

4.1.1 獲取偏向鎖

(a) 前提條件

獲取偏向鎖的前提條件是synchronized所修飾的對象處於可偏向狀態

  • 鎖狀態爲01
  • 偏向鎖狀態爲1

(b) 獲取過程

當知足前提條件時,再去判斷對象的Mark Word中的線程ID是否指向當前線程

  • 若是不指向當前線程,那麼經過CAS操做競爭鎖
    • 競爭成功:將Mark Word的線程ID替換爲當前線程ID,接着執行同步代碼塊
    • 競爭失敗:證實存在多線程競爭的狀況,當到達全局安全點,得到偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,而後被阻塞在安全點的線程繼續往下執行同步代碼塊
  • 若是指向當前線程,那麼執行同步代碼塊

4.1.2 釋放偏向鎖

(a) 前提條件

釋放偏向鎖的前提條件是其它的線程在競爭偏向鎖的過程當中出現了失敗的狀況,而且偏向鎖的釋放須要等待到達全局安全點。

(b) 釋放過程

當知足釋放偏向鎖的前提條件時,首先會暫停擁有偏向鎖的線程,接着判斷鎖對象是否處於被鎖定的狀態,決定鎖標誌位下一步的狀態:

  • 若是未被鎖定,那麼將鎖標誌至爲01,偏向鎖狀態置爲0,表示它處於無鎖,且不可偏向狀態。
  • 若是已經被鎖定,那麼將鎖標誌置爲00,表示它處於被輕量級鎖定的狀態。

4.2 輕量級鎖狀態

引入輕量級鎖的目的是:在無多線程競爭的狀況下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。

4.2.1 獲取輕量級鎖

(a) 前提條件

獲取輕量級鎖的前提條件時當前對象處於無鎖狀態,

  • 鎖狀態標誌位爲01
  • 偏向鎖標誌位爲0

(b) 獲取過程

JVM首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲對象目前的Mark Word的拷貝,以後JVM利用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針:

  • 操做成功:將鎖標誌置爲00,表示處於鎖定的狀態,以後執行同步操做。
  • 操做失敗:那麼檢查對象的Mark Word是否指向當前線程的棧針
  • 若是是,則直接執行同步代碼塊
  • 若是不是,說明該鎖對象已經被其餘線程搶佔了,此時輕量級鎖升級爲重量鎖,鎖標誌位變爲10,後面等待的線程將會進入阻塞狀態。

4.2.2 釋放輕量級鎖

(a) 釋放過程

輕量級鎖的釋放也是經過CAS操做來進行的:

  • 取出在獲取輕量級鎖時,保存在Displaced Mark Word中的數據。
  • CAS操做將取出的數據替換到當前對象的Mark Word中:
  • 若是成功,則說明釋放鎖成功
  • 若是失敗,說明有其它線程嘗試獲取該鎖,那麼須要在釋放鎖的同時,喚醒須要被喚醒的線程

對於輕量級鎖,它性能提高的依據是默認"對於絕大部分的鎖,在整個生命週期內是不會存在競爭的",若是不符合這種狀況,那麼除了互斥的開銷外,還有額外的CAS操做,這樣輕量級鎖比重量級鎖更慢。

5、參考文章

Java 併發編程:Synchronized 底層優化(偏向鎖、輕量級鎖) 死磕 Java 併發 -----深刻分析 synchronized 的實現原理

相關文章
相關標籤/搜索