Java中的鎖系列3

上一章我們講了synchronized對象鎖的各種級別。但是,有的時候會被人問到ReentrantLock是怎么實現的么?AQS是啥?公平鎖和非公平鎖怎么實現的么?排他鎖和共享鎖怎么實現么?

就算我們弄懂了前兩章的內容,還是只能不好意思的回答:不會。

相關的類

在文章的開頭,我自己畫了一個相關的類的類圖,讓你可以在開始的時候有個大概的了解。在讀文章的過程中,你也可以時不時回來看一下這個圖,應該會有很大的收獲。


ReentrantLock

首先我們從ReentrantLock的構造函數入手,來看下ReentrantLock是怎么實現的。


ReentrantLock有兩種實現,FairSync公平鎖的實現和NonfairSync非公平鎖的實現。

ReentrantLock的lock和unlock也是調用的對應sync里的lock和unlock。

FairSync和NonfairSync都繼承了Sync類,而Sync則繼承了AbstractQueuedSynchronizer, AbstractQueuedSynchronizer也就是傳說中的AQS,AQS繼承了AbstractOwnableSynchronizer,我們可以把AbstractOwnableSynchronizer叫AOS(這個名字是我自己取的)。

NonfairSync

NonfairSync的代碼特別少,只有兩個方法lock()和tryAcquire()。


我看到這個代碼第一映像是:不是應該lock()跟unlock()方法么,怎么會是lock()和tryAcquire()呢?

實際上,我們看下ReentrantLock的lock()方法和unlock()方法:



也就是說lock調的是NonfairSync的lock()方法,unlock調用的應該是NonfairSync的release()方法。

relase()

不管NonfairSync還是FairSync,release()方法都是在他們的父類AQS里實現的。這意味著,公平鎖和非公平鎖,釋放鎖的步驟是一樣的。

下面是AQS里的release的代碼。


這個就解釋了為什么NonfairSync和FairSync里只有lock了。


lock()

NonfairSync里的lock()我們知道是干什么的了,那么tryAcquire()又是個什么鬼呢?

NonfairSync的實現里tryAcquire()只有一行,return nonfairTryAcquire(acquires)。

我們再來看下nonfairTryAcquire方法。


通過讀nonfairTryAcquire()這個方法,我們知道,實際上這個方法就是試著通過state和對應的thread值去獲取鎖,獲取成功了返回true,失敗了返回false。

如果當前線程已經獲取到鎖了,那么再次調用lock的時候,nextc實際上是+1,然后將nextc設置到state里去。這樣也實現了ReentrantLock的可重入性。

但是,問題來了,tryAcquire這個方法在哪里調用呢?

我們再回過頭來看一下lock方法


compareAndSetState()方法就是對state的一個CAS操作,期望值0,修改值1。成功了,就意味著拿到鎖了。

acquire()方法,是在AQS里實現的。我們看下acquire的代碼。


好了,找到tryAcquire()的調用地方了:實際上在lock里調用的。只不過tryAcquire在AQS里是一個虛方法。真正實現其實還是在子類也就是NonfairSync和FairSync。

FairSync

我們在來看一下FairSync類的實現。


同樣很簡單,跟NonfairSync的區別, 無非就是lock方法少了一次CAS操作,然后tryAcquire方法多了一個hasQueuedPredecessors()的判斷。

這意味著hasQueuedPredecessors()的判斷,很可能是用來保證鎖的公平性的。

鎖的公平實際上就是指先進等待隊列的先拿到鎖。我們從hasQueuedPredecessors的方法名字中其實就可以看出,這個方法就是為了保證當前獲取鎖的線程前面沒有其他等待獲取鎖的線程了,如果有其他線程在前面,當前線程就拿不到鎖。這樣也確實是可以保證得到鎖的順序為先來先得,也就是保證鎖的公平性。

到這里對NonfairSync和FairSync的分析就告一段落了。lock已經轉移到了對AQS里acquire方法的分析,unlock已經轉移到了對AQS里release方法的分析。

AQS

AbstractQueuedSynchronizer,傳說中的AQS。

AQS其實是一種變種的CLH鎖。關于CLH的資料可以看這個blog:

http://blog.csdn.net/aesop_wubo/article/details/7533186

前面我們說到了lock()已經轉移到了AQS里的acquire()方法。那我們就從acquire()作為入口開始了解AQS。

Acquire獲取鎖的流程如下:


我們在來看下acquire()的代碼:


首先通過tryAcquire()嘗試獲取鎖,如果獲取不到,就創建一個代表當前線程的節點加入到等待隊列的尾部,這個節點的模式為EXCLUSIVE也就是排他模式,對應的還有一個模式是SHARE,也就是共享模式。

tryAcquire()方法我們前面已經講過了,下面主要看一下addWaiter()方法和acquireQueued()方法。

addWaiter

通過addWaiter()的方法名,我們就可以知道這個方法是往等待隊列里加入一個Node。


這個方法應該分為兩部分來讀:

tail != null和tail == null。

tail != null表示當前隊列里有元素,那么就設置當前節點為隊尾節點。

tail== null表示當前隊列里沒有元素,那么就執行enq()方法,將這個節點入隊。

其實我也疑惑,這里直接調enq方法不就行了嗎?為什么還要進行一次判斷呢?希望有大神能給解釋下~

我們再看下enq()方法


enq()方法采用的就是變種CLH算法,先看尾結點是否為空,如果為空就創建一個傀儡結點,頭尾指針都指向這個傀儡結點,這一步只會在隊列初始化時會執行;

如果尾結點非空,就采用CAS操作將當前結點插入到尾結點后面,如果在插入的時候尾結點有變化,就將尾結點向后移動直到移動到最后一個結點為止,然后再把當前結點插入到尾結點后面,尾指針指向當前結點,入隊成功。

acquireQueued

我們再來看一下acquireQueued()方法。


將新加入的結點放入隊列之后,這個結點有兩種狀態,要么獲取鎖,要么就掛起。

如果這個節點的前趨節點是頭節點,就會用tryAcquire()來嘗試獲取鎖。如果成功,就將當前節點設置為頭節點,并且將前趨節點設置為null,這樣方便前趨節點被GC回收。如果失敗就繼續在for循環里獲取,直到獲取成功。

如果這個結點不是頭結點,就看看這個結點是否應該掛起,如果應該掛起,就掛起當前結點,是否應該掛起是通過shouldParkAfterFailedAcquire()方法來判斷的。


看到這里的Node.SIGNAL,可能很多人也疑惑了,這些狀態都代表什么意思呢?

CLH鎖的本質是一個FIFO的隊列,既然是隊列, 那肯定有一個個的節點。

CLH鎖的節點里,只有true和false的狀態。但是AQS的節點有很多。

具體的狀態值如下:

static final int CANCELLED =1;

static final int SIGNAL= -1;

static final int CONDITION = -2;

static final int PROPAGATE = -3;

下面解釋下這些節點狀態的具體含義:

CANCELLED:因為超時或者中斷,結點會被設置為取消狀態,被取消狀態的結點不應該去競爭鎖,只能保持取消狀態不變,不能轉換為其他狀態。處于這種狀態的結點會被踢出隊列,被GC回收;

SIGNAL:表示這個結點的繼任結點被阻塞了,到時需要通知它;

CONDITION:表示這個結點在條件隊列中,因為等待某個條件而被阻塞;

PROPAGATE:使用在共享模式頭結點有可能牌處于這種狀態,表示鎖的下一次獲取可以無條件傳播;

0:None of the above,新結點會處于這種狀態。

了解了狀態,我們再來看看這個方法。

該方法有兩個參數,pred和node。

pred為當前節點的前趨節點。

node為當前節點。

該方法首先檢查前趨結點的waitStatus位,如果為SIGNAL,表示前趨結點會通知它,那么它可以放心大膽地掛起了;

如果waitStatus>0也就是前趨結點是一個被取消的結點怎么辦呢?那么就向前遍歷跳過被取消的結點,直到找到一個沒有被取消的結點為止,將找到的這個結點作為它的前趨結點,將找到的這個結點的waitStatus位設置為SIGNAL,返回false表示線程不應該被掛起。

如果不被掛起 , 那就會一直在acquireQueued方法的for循環里執行。


對于AQS,我自己也在讀代碼的階段,如果后面發現有什么不對的地方,或者有理解更透徹的地方,我會更新本章節,也歡迎大家評論指正。

參考資料:

http://blog.csdn.net/aesop_wubo/article/details/7555956

http://ifeve.com/java-special-troops-aqs/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容