》》》歸屬專欄:《高并發的暴力美學》
一、前情概要
多線程大師 Doug Lea 被稱為 世界上對Java影響力最大的個人,這個鼻梁挂着眼鏡,留着德王威廉二世的胡子,臉上永遠挂着謙遜腼腆笑容,服務于紐約州立大學Oswego分校計算機科學系的老大爺。
本篇是學習 Doug Lea大師的論文《The java.util.concurrent Synchronizer Framework》 JUC 同步器架構(AQS 架構)原文翻譯 ,并結合對 AQS 的源碼的梳理後的形成的一些思考總結,記錄下來。在對 AQS 的 ReentrantLock 的源碼閱讀分析後,也整理出了核心流程腦圖 ,感興趣的 》》》【點選檢視腦圖】。
本篇是自省的方式來梳理論文原文,非學術那麼嚴謹,目的是為了更友善了解其設計與實作,如對 AQS 不太了解,也可以直接閱讀本篇,但需要耐心。若是對 AQS 比較熟悉,那更歡迎閱讀本篇,很期待讀者老師的交流指正。
二、概念認知對齊
2.1 互斥
互斥,是讓線程交替執行一段代碼,而不能同時執行;換一個說法就是線程搶到鎖後就執行同步代碼,搶不到鎖(同步狀态)就不能執行同步代碼,隻能等待;而等待可以表現出 3 種方式:
- 争取不到鎖,線程休眠等待
- 争取不到鎖,線程自旋等待
- 争取不到鎖,線程先自旋等待一會兒,不行再休眠等待(AQS 的做法)
2.2 AQS 中鎖的關鍵特征描述
1)鎖狀态
在 AQS 中鎖狀态,使用 int 類型的狀态變量來記錄,通過 CAS 操作來修改其值;
- 0表示無鎖
- 1表示加鎖
- >1 表示重入,重入時執行++操作,記錄重入次數
- 釋放鎖時執行--操作,直到減到0才表示鎖被完全釋放,可被其他線程搶占
2)休眠與喚醒
休眠和喚醒使用unsafe中的park(休眠線程)和unpark(喚醒線程)方法。
3)自旋
自旋使用for循環,循環次數是有限的。
4)等待
等待使用雙向隊列的結構來實作排隊等待的效果,不能插隊的模式是公平模式,可插隊模式是非公平模式。
三、單向隊列的需求及設計的思考
從原文這一段開始:
在原始版本的 CLH 鎖中,節點間甚至都沒有互相連結。自旋鎖中,pred 變量可以是一個局部變量。然而,Scott 和 Scherer 證明了通過在節點中顯式地維護前驅節點,CLH 鎖就可以處理“逾時”和各種形式的“取消”:如果一個節點的前驅節點取消了,這個節點就可以滑動去使用前面一個節點的狀态字段。
了解原文:【單向隊列】可以比較容易的處理逾時和取消請求,在隊列的組織結構下,前一個請求節點因為取消或者逾時而變得報廢無效後,後邊的請求有往前推進需求和能力,可以把無效的請求節點移除隊列,以便讓處于存活有效狀态的向前移動。
- 問題:為什麼選擇 tail 向 head 方向(pre 鍊路)?
- 從tail到head,即pre方向更自然,隊末的更有需求讓隊伍往前走,因為自己的事還沒辦;隊首的自己的事已經辦完了,後邊排隊的跟自己關系不大。 複制代碼
- 想象一下現實開車時的超車狀态:你要往前走,前車若因異常(類比線程逾時、取消狀态)不能前進,你在後邊能感覺到,因為有繼續前行的需求,你會繞開繼續前行。但若是你的後車有問題,你應該是不管的吧。
- 問題: 自旋修改為自旋+阻塞
- 原文說:
- 第二個對 CLH 隊列主要的修改是将每個節點都有的狀态字段用于控制阻塞而非自旋”,... “取消”狀态必須存在于狀态字段中"。
- 這句話的關鍵之處在于【把面向自旋的設計修改為了面向阻塞】,自旋的設計要了解的關鍵點在于:每一個節點的“釋放”狀态,是儲存在其前驅節點中;是以,自旋鎖的“自旋”操作就如下:
- while (pred.status != RELEASED); //自旋後的出隊操作隻需将head字段指向剛剛得到鎖的節點: head = node; 複制代碼
- 但是全部是自旋挺浪費 CPU ,于是考慮修改成阻塞試試:
- while (pred.status != RELEASED){ 阻塞休眠 } 複制代碼
- 但讓所有線程節點一上來看到狀态不滿足,就阻塞也不合适;比如第一個排隊的,跟其他後續排隊的情況不同,對于第一個排隊的,其前邊的可能已經或者很快就結束,這種情況下最好先自旋幾次嘗試去搶鎖,如果沒搶成功再阻塞,而其他後續排隊的節點一看前邊有排隊的了,自己則可以直接排隊阻塞。
- 原文中”檢查目前節點的前驅是否為head來确定權限"也是這個意思,即不用判斷前驅節點的release狀态,隻需要判斷是不是head,是head就嘗試競争鎖,不是head就阻塞(park)排隊,等着被喚醒。
四、變成雙向隊列的需求及設計的思考
- 問題:為什麼要變成雙向隊列?
- 線程拿不到鎖會排隊等待,目前邊線程釋放鎖之後,後續就需要【被喚醒】繼續工作。【那如何喚醒後繼的節點】,前邊提到 隊列被設計為tail -> head方向的鍊路,如果從tail順着pre往head方向一直盯着,總能感覺到是否輪到自己了,但這樣性能不高,可以優化一下,若釋放鎖的節點能直接找到自己的後繼節點,通知它拿鎖幹活兒就會更友善。是以增加 next 方向,隊列就變成了雙向。
- 但是雙向的鍊路控制并非原子性的,總得有取舍,AQS 采取保證pre方向及時有效,next方向略有延遲,是以,在next方向找不到的情況下,會嘗試從tail沿着pre方向往前找一下。
- 問題: 後繼節點如何阻塞,如何喚醒?
- 目前節點release後,後繼節點可能正在自旋競争鎖,這種情況下沒有必要去喚醒它(雖然unpark喚醒操作也不會出錯,但也是有成本的);出于成本考慮再優化一下,在休眠之前,最好給前驅節點個“喚醒(signal me)”自己的信号。
- 是以線程調用park前,給前驅節點設定一個“喚醒(signal me)”标志,并再嘗試一次去拿鎖,如果競争到了鎖,就避免了一次不必要的阻塞;如果競争不到鎖,才去真的休眠。
- 問題:隊列為何是延遲初始化?
- 隻有一個線程的時候,或者多個線程之間是交替執行,且線程的執行在時序無重疊,這種情況下,線程都不需要排隊,隻需要判斷同步狀态,修改同步狀态;是以隊列是在首次需要的時候才進行初始化(建構一個虛拟節點,head和tail都指向它)。
- 問題:公平與非公平是排隊政策不同?
- 非公平:插隊模式,往隊首插隊,插不進去,才追加到隊尾。
- 公平:不可插隊模式,到隊尾排隊。
四、最後說一句
我是石頁兄,如果這篇文章對您有幫助,或者有所啟發的話,歡迎關注筆者的微信公衆号【 架構染色 】進行交流和學習。您的支援是我堅持寫作最大的動力。