java 併發(六) --- 鎖
閱讀前閱讀以下參考資料,文章圖片或程式碼部分來自與參考資料
概覽
一張圖瞭解一下java鎖.
各種鎖
為什麼要設定鎖的等級
jdk1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。
鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。
樂觀鎖和悲觀鎖
閱讀這篇文章,瞭解樂觀鎖和悲觀鎖 ---- 不可不說的Java“鎖”事
我們可以知道 :
- 樂觀鎖與悲觀鎖的區分在於有沒有對同步資源上鎖.
- 樂觀鎖沒有上鎖,保證執行緒同步的常用實現是 CAS
- 悲觀鎖適合寫操作多的場景,先加鎖可以保證寫操作時資料正確。
- 樂觀鎖適合讀操作多的場景,不加鎖的特點能夠使其讀操作的效能大幅提升。
自旋鎖和自適應自旋鎖
解決的通點 :
在介紹自旋鎖前,我們需要介紹一些前提知識來幫助大家明白自旋鎖的概念。
阻塞或喚醒一個Java執行緒需要作業系統切換CPU狀態來完成,這種狀態轉換需要耗費處理器時間。如果同步程式碼塊中的內容過於簡單,狀態轉換消耗的時間有可能比使用者程式碼執行的時間還要長。
在許多場景中,同步資源的鎖定時間很短,為了這一小段時間去切換執行緒,執行緒掛起和恢復現場的花費可能會讓系統得不償失。如果物理機器有多個處理器,能夠讓兩個或以上的執行緒同時並行執行,我們就可以讓後面那個請求鎖的執行緒不放棄CPU的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖。
而為了讓當前執行緒“稍等一下”,我們需讓當前執行緒進行自旋,如果在自旋完成後前面鎖定同步資源的執行緒已經釋放了鎖,那麼當前執行緒就可以不必阻塞而是直接獲取同步資源,從而避免切換執行緒的開銷。這就是自旋鎖。
自適應鎖
JDK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。所謂自適應就意味著自旋的次數不再是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。它怎麼做呢?執行緒如果自旋成功了,那麼下次自旋的次數會更加多,因為虛擬機器認為既然上次成功了,那麼此次自旋也很有可能會再次成功,那麼它就會允許自旋等待持續的次數更多。反之,如果對於某個鎖,很少有自旋能夠成功的,那麼在以後要或者這個鎖的時候自旋的次數會減少甚至省略掉自旋過程,以免浪費處理器資源。
有了自適應自旋鎖,隨著程式執行和效能監控資訊的不斷完善,虛擬機器對程式鎖的狀況預測會越來越準確,虛擬機器會變得越來越聰明。
即是說這次成功獲取到鎖,那麼就意味著該執行緒獲取到鎖的機率比較大,那麼執行自旋的次數就會多些.
在自旋鎖中 另有三種常見的鎖形式:TicketLock、CLHlock和MCSlock,本文中僅做名詞介紹,不做深入講解,感興趣的同學可以自行查閱相關資料。
無鎖 , 偏向鎖 ,輕量級鎖 ,重量級鎖
這四種鎖是指鎖的狀態,專門針對synchronized的。在介紹這四種鎖狀態之前還需要介紹一些額外的知識。首先為什麼Synchronized能實現執行緒同步?在回答這個問題之前我們需要了解兩個重要的概念:“Java物件頭”、“Monitor”。
Java 物件頭
下面內容引用來自 【死磕Java併發】—–深入分析synchronized的實現原理
synchronized用的鎖是存在Java物件頭裡的,那麼什麼是Java物件頭呢?Hotspot虛擬機器的物件頭主要包括兩部分資料:Mark Word(標記欄位)、Klass Pointer(型別指標)。其中Klass Point是是物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項,Mark Word用於儲存物件自身的執行時資料,它是實現輕量級鎖和偏向鎖的關鍵,所以下面將重點闡述
Mark Word
Mark Word用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒 ID、偏向時間戳等等。Java物件頭一般佔有兩個機器碼(在32位虛擬機器中,1個機器碼等於4位元組,也就是32bit),但是如果物件是陣列型別,則需要三個機器碼,因為JVM虛擬機器可以通過Java物件的元資料資訊確定Java物件的大小,但是無法從陣列的元資料來確認陣列的大小,所以用一塊來記錄陣列長度。下圖是Java物件頭的儲存結構(32位虛擬機器):
物件頭資訊是與物件自身定義的資料無關的額外儲存成本,但是考慮到虛擬機器的空間效率,Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲存儘量多的資料,它會根據物件的狀態複用自己的儲存空間,也就是說,Mark Word會隨著程式的執行發生變化,變化狀態如下(32位虛擬機器):
總結一下java頭物件資訊
Monitor
什麼是Monitor?我們可以把它理解為一個同步工具,也可以描述為一種同步機制,它通常被描述為一個物件。
與一切皆物件一樣,所有的Java物件是天生的Monitor,每一個Java物件都有成為Monitor的潛質,因為在Java的設計中 ,每一個Java物件自打孃胎裡出來就帶了一把看不見的鎖,它叫做內部鎖或者Monitor鎖。
Monitor 是執行緒私有的資料結構,每一個執行緒都有一個可用monitor record列表,同時還有一個全域性的可用列表。每一個被鎖住的物件都會和一個monitor關聯(物件頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner欄位存放擁有該鎖的執行緒的唯一標識,表示該鎖被這個執行緒佔用。其結構如下:
- Owner:初始時為NULL表示當前沒有任何執行緒擁有該monitor record,當執行緒成功擁有該鎖後儲存執行緒唯一標識,當鎖被釋放時又設定為NULL;
- EntryQ:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的執行緒。
- RcThis:表示blocked或waiting在該monitor record上的所有執行緒的個數。
- Nest:用來實現重入鎖的計數。
- HashCode:儲存從物件頭拷貝過來的HashCode值(可能還包含GC age)。
- Candidate:用來避免不必要的阻塞或等待執行緒喚醒,因為每一次只有一個執行緒能夠成功擁有鎖,如果每次前一個釋放鎖的執行緒喚醒所有正在阻塞或等待的執行緒,會引起不必要的上下文切換(從阻塞到就緒然後因為競爭鎖失敗又被阻塞)從而導致效能嚴重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的執行緒1表示要喚醒一個繼任執行緒來競爭鎖。
有了這種結構我們就可以實現自旋了,因為Monitor的資料結構可以讓其他執行緒判斷鎖是否釋放.
無鎖
無鎖沒有對資源進行鎖定,所有的執行緒都能訪問並修改同一個資源,但同時只有一個執行緒能修改成功。
無鎖的特點就是修改操作在迴圈內進行,執行緒會不斷的嘗試修改共享資源。如果沒有衝突就修改成功並退出,否則就會繼續迴圈嘗試。如果有多個執行緒修改同一個值,必定會有一個執行緒能修改成功,而其他修改失敗的執行緒會不斷重試直到修改成功。上面我們介紹的CAS原理及應用即是無鎖的實現。無鎖無法全面代替有鎖,但無鎖在某些場合下的效能是非常高的。
輕量級鎖
引入輕量級鎖的主要目的是在多沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗。當關閉偏向鎖功能或者多個執行緒競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖,其步驟如下:
獲取鎖
- 判斷當前物件是否處於無鎖狀態(hashcode、0、01),若是,則JVM首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced字首,即Displaced Mark Word);否則執行步驟(3);
- JVM利用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指正,如果成功表示競爭到鎖,則將鎖標誌位變成00(表示此物件處於輕量級鎖狀態),執行同步操作;如果失敗則執行步驟(3);
- 判斷當前物件的Mark Word是否指向當前執行緒的棧幀,如果是則表示當前執行緒已經持有當前物件的鎖,則直接執行同步程式碼塊;否則只能說明該鎖物件已經被其他執行緒搶佔了,這時輕量級鎖需要膨脹為重量級鎖,鎖標誌位變成10,後面等待的執行緒將會進入阻塞狀態;
釋放鎖
輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:
- 取出在獲取輕量級鎖儲存在Displaced Mark Word中的資料;
- 用CAS操作將取出的資料替換當前物件的Mark Word中,如果成功,則說明釋放鎖成功,否則執行(3);
- 如果CAS操作替換失敗,說明有其他執行緒嘗試獲取該鎖,則需要在釋放鎖的同時需要喚醒被掛起的執行緒。
對於輕量級鎖,其效能提升的依據是“對於絕大部分的鎖,在整個生命週期內都是不會存在競爭的”,如果打破這個依據則除了互斥的開銷外,還有額外的CAS操作,因此在有多執行緒競爭的情況下,輕量級鎖比重量級鎖更慢;
下圖是輕量級鎖的獲取和釋放過程
偏向鎖
引入偏向鎖主要目的是:為了在無多執行緒競爭的情況下儘量減少不必要的輕量級鎖執行路徑。上面提到了輕量級鎖的加鎖解鎖操作
是需要依賴多次CAS原子指令的。那麼偏向鎖是如何來減少不必要的CAS操作呢?我們可以檢視Mark work的結構就明白了。只需要檢查
是否為偏向鎖、鎖標識為以及ThreadID (標識)即可,處理流程如下:
獲取鎖
- 檢測Mark Word是否為可偏向狀態,即是否為偏向鎖1,鎖標識位為01;
- 若為可偏向狀態,則測試執行緒ID是否為當前執行緒ID,如果是,則執行步驟(5),否則執行步驟(3);
- 如果執行緒ID不為當前執行緒ID,則通過CAS操作競爭鎖,競爭成功,則將Mark Word的執行緒ID替換為當前執行緒ID,否則執行步驟(4);
- 通過CAS競爭鎖失敗,證明當前存在多執行緒競爭情況,當到達全域性安全點(在這個時間點上沒有位元組碼正在執行),獲得偏向鎖的執行緒被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的執行緒繼續往下執行同步程式碼塊;
- 執行同步程式碼塊
可以看到在步驟3的時候,要是成功,只會發生一次CAS 交換.同時我們也可以看到,隨著競爭的激烈程度,會導致鎖的層次逐漸上升上去.
偏向鎖在JDK 6及以後的JVM裡是預設啟用的。可以通過JVM引數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之後程式預設會進入輕量級鎖狀態。
釋放鎖
偏向鎖的釋放採用了一種只有競爭才會釋放鎖的機制,執行緒是不會主動去釋放偏向鎖,需要等待其他執行緒來競爭。偏向鎖的撤銷需要等待全域性安全點(這個時間點是上沒有正在執行的程式碼)。其步驟如下:
- 暫停擁有偏向鎖的執行緒,判斷鎖物件石是否還處於被鎖定狀態;
- 撤銷偏向蘇,恢復到無鎖狀態(01)或者輕量級鎖的狀態;
下圖是偏向鎖的獲取和釋放流程
重量級鎖
重量級鎖通過物件內部的監視器(monitor)實現,其中monitor的本質是依賴於底層作業系統的Mutex Lock實現,作業系統實現執行緒之間的切換需要從使用者態到核心態的切換,切換成本非常高。
鎖的流程如下 :
公平鎖 和 非公平鎖
ReentrantLock 內部實現了公平鎖和非公平鎖。
公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖,執行緒直接進入佇列中排隊,佇列中的第一個執行緒才能獲得鎖。公平鎖的優點是等待鎖的執行緒不會餓死。缺點是整體吞吐效率相對非公平鎖要低,等待佇列中除第一個執行緒以外的所有執行緒都會阻塞,CPU喚醒阻塞執行緒的開銷比非公平鎖大。
非公平鎖是多個執行緒加鎖時直接嘗試獲取鎖,獲取不到才會到等待佇列的隊尾等待。但如果此時鎖剛好可用,那麼這個執行緒可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現後申請鎖的執行緒先獲取鎖的場景。非公平鎖的優點是可以減少喚起執行緒的開銷,整體的吞吐效率高,因為執行緒有機率不阻塞直接獲得鎖,CPU不必喚醒所有執行緒。缺點是處於等待佇列中的執行緒可能會餓死,或者等很久才會獲得鎖。
下面的圖可以解釋這兩種的差別 :
上圖.公平鎖 下圖.非公平鎖
接下來看看具體的原始碼實現
ReentranLock 和 Synchronized 的區別
- reentranLock 可重入,同時公平鎖和非公平鎖和Synchronized 獲取鎖的方式對比,具有效能優勢(非公平鎖吞吐量大)。
- synchronized與Lock在預設情況下是不會響應中斷(interrupt)操作,會繼續執行完。lockInterruptibly()提供了可中斷鎖來解決此問題。
鎖優化
鎖消除和鎖粗化
這方面的內容請看 【死磕Java併發】—–深入分析synchronized的實現原理