最近一段時間在研究jdk裡的concurrent包,分為了線程管理,鎖操作以及原子操作三個部分。線程管理平時用得還算多,但是鎖操作和原子操作基本就沒用過,隻是以前在大學的時候跑了幾個例子玩玩。當看到reentrantlock的時候,發現用法倒是和synchronized有點類似也很簡單,但是内部原理比較複雜。網上查了關于reentrantlock的相關内容,沒發現有誰把它分析得很透徹,隻是有幾篇講了内部的鎖實作機制,隻可惜都是以文字為主,難以把整個内部流程串起來了解,相信很多人沒有真正把這個新的同步機制搞明白,當然也包括我在内。是以花了幾天時間反複調試,終于把最基礎的lock-unlock以及與reentrantlock綁定的condition的await-signal機制以流程圖的形式畫了出來,同時把我認為較為難了解的代碼也分析了其内部原理加以注釋,以此分享給大家,希望能幫助各位更快的了解它的實作原理,也歡迎批評指正。
目前隻是研究了正常狀态下的同步原理,如果存線上程中斷,其流程将更為複雜。是以暫時先上兩篇無中斷的流程講解,待我能把帶中斷的流程整理清楚了再發出來,以免誤導大家。
在jdk1.5之前,多線程之間的同步是依靠synchronized來實作。synchronized是java的關鍵字,直接由jvm解釋成為指令進行線程同步管理。因為操作簡單,而且現在jdk的後續版本已經對synchronized進行了很多的優化,是以一直是大家編寫多線程程式常用的同步工具。那為什麼要推出新的同步api呢?jdk1.5釋出的時候,synchronized性能并不好,這可能是concurrent包出現的一個潛在原因,但是更重要的是新的api提供了更靈活,更細粒度的同步操作,以滿足不同的需求。但是問題也很明顯,越靈活可控度越高就越容易出錯,是以大多數人很少使用concurrent裡的同步鎖api。本文并不比較兩者的優劣,畢竟存在即合理。能把更進階的工具用好,有時候提高工作效率,加快異常排查也是很不錯的,不過本人是出于學習其原理及思想才進行研究,工作中用哪一個同樣也會謹慎抉擇。
reentrantlock類圖:
abstractownablesynchronizer類保持和擷取獨占線程。
abstractqueuedsynchronizer是以虛拟隊列的方式管理線程的鎖擷取與鎖釋放,以及各種情況下的線程中斷。提供了預設的同步實作,但是擷取鎖和釋放鎖的實作定義為抽象方法,由子類實作。目的是使開發人員可以自由定義擷取鎖以及釋放鎖的方式。
sync是reentrantlock的内部抽象類,實作了簡單的擷取鎖和釋放鎖。
nonfairsync和fairsync分别表示“非公平鎖”和“公平鎖”,都繼承于sync,并且都是reentrantlock的内部類。
reentrantlock實作了lock接口的lock-unlock方法,根據fair參數決定使用nonfairsync還是fairsync。
這裡面有兩個重點内容:
abstractqueuedsynchronizer内部的node
abstractqueuedsynchronizer内部的state
node:
node是對每一個通路同步代碼的線程的封裝。不僅包括了需要同步的線程,而且也包含了每個線程的狀态,比如等待解除阻塞,等待條件喚醒,已經被取消等等。同時node還關聯了前驅和後繼,即prev和next。個人認為是為了以集中的方式管理多個不同狀态的線程,當不同的線程發生狀态改變時,可以盡快的反應到别的線程上,提高運作效率。比如某個node的prev已經被取消了,那麼當對這個prev解除阻塞的時候就可以被忽略掉,進而嘗試解除該node的阻塞狀态。
多個node連接配接起來成為了虛拟隊列(因為不存在真正的隊列容器将每個元素裝起來是以說是虛拟的,我把它稱為release隊列,意思是等待釋放),那麼就得有head和tail。針對公平鎖,head是不帶線程的特殊node,隻有next,而最新一個請求鎖的線程取鎖失敗時就把它添加到隊尾,即tail。但是對于非公平鎖,新請求鎖的線程會插隊,也許會插到最前面,也許不會。
這裡可能有人會有疑問:head放在隊列中有什麼用處?為什麼不是一個等待鎖的線程作為head呢?原因很簡單,因為每個等待線程都有可能被中斷而取消,對于一個已經取消的線程,自然是有機會就把它gc了。那麼gc前一定得讓後續的node成為head,這樣一來sethead的操作過于分散,而且要應對多種線程狀态的變化來設定head,這樣就太麻煩了。是以這裡很巧妙地将head的next設定為等待鎖的node,head就相當于一個引導的作用,因為head沒有線程,是以不存在“取消”這種狀态。
state:
state是用來記錄鎖的持有情況。
沒有線程持有鎖的時候,state為0。
當某個線程擷取鎖時,state的值增加,具體增加多少開發人員可自定義,預設為1,表示該鎖正在被一個線程占有。
當某個已經占用鎖的線程再次擷取到鎖時,state再增長,此為重入鎖。
當占有鎖的線程釋放鎖時,state也要減去當初占有時傳入的值,預設為1。
多個線程競争鎖的時候,state必須通過cas進行設定,這樣才能保證鎖隻能有一個線程持有。當然這是排它鎖的規則,共享鎖就不是這樣了。
公平鎖和非公平鎖的不同僅在于修改state的時機。看下面的代碼就能明白:
// reentrantlock.class
protected final boolean tryacquire(int acquires) {
final thread current = thread.currentthread();
int c = getstate();
if (c == 0) {
// 此為公平鎖的實作,而非公平鎖不調用hasqueuedpredecessors方法,即不需要判斷隊列裡是否有内容,直接通過cas修改state來競争鎖
if (!hasqueuedpredecessors() &&
compareandsetstate(0, acquires)) {
setexclusiveownerthread(current);
return true;
}
}
else if (current == getexclusiveownerthread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new error("maximum lock count exceeded");
setstate(nextc);
return true;
return false;
}
基本上把node和state的意義弄明白了,一個正常的lock-unlock過程應該很容易明白。但是代碼裡有很多地方要針對已取消的線程做特殊處理,是以了解上還是會有困難。因為已經有幾篇文章進行了源碼講解,是以這裡我就不再把每段源代碼都拿出來細講了。
先看看lock()的流程:(圖中的node0和node1在源代碼中不存在,是我為了友善說明清楚才添加的别稱)
如圖所示,彩色的字都是一個cas操作,其中三個紅色cas都對成功和失敗有相應地處理,為什麼另外一個藍色cas不關心設定是否成功呢?下面這段代碼裡我給出了解釋:
// abstractqueuedsynchronizer.class
private static boolean shouldparkafterfailedacquire(node pred, node node) {
int ws = pred.waitstatus;
if (ws == node.signal)
/*
* this node has already set status asking a release
* to signal it, so it can safely park.
*/
if (ws > 0) {
* predecessor was cancelled. skip over predecessors and
* indicate retry.
do {
node.prev = pred = pred.prev;
} while (pred.waitstatus > 0);
pred.next = node;
} else {
* waitstatus must be 0 or propagate. indicate that we
* need a signal, but don't park yet. caller will need to
* retry to make sure it cannot acquire before parking.
* 為什麼不關心是否成功卻還要設定呢?
*
* 如果設定失敗,表示前驅已經被signal了。如果前驅是head,說明有機會擷取鎖,是以傳回false後還可以再次tryacquire
* 如果設定成功,表示前驅等待signal。如果再次确認pred.waitstatus仍然是node.signal,則表明前驅等待釋放鎖的情況下必須阻塞目前線程
* 是以傳回true後即被park
compareandsetwaitstatus(pred, ws, node.signal);
再看下unlock()的流程圖:
如圖所示,這裡的粉紅色折線與lock流程圖裡的粉紅色虛折線對應,即線程a調用lock阻塞與線程b調用unlock解除線程a的阻塞。同時可以看到unlock隻有一個cas操作,但是也不用關心設定是否成功。我給這段代碼做了下面解釋:
private void unparksuccessor(node node) {
/*
* if status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. it is ok if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitstatus;
* 為什麼不關心是否成功卻還要設定呢?
*
* 注意這裡的node實際就是head
*
* 如果設定成功,即head.waitstatus=0,則可以讓這時即将被阻塞的線程有機會再次調用tryacquire擷取鎖。
* 也就是讓shouldparkafterfailedacquire方法裡的compareandsetwaitstatus(pred, ws, node.signal)執行失敗傳回false,這樣就能再有機會再tryacquire了
* 如果設定失敗,新跟随在head後面的線程被阻塞,但是沒關系,下面的代碼會立即将這個阻塞線程釋放掉
if (ws < 0)
compareandsetwaitstatus(node, ws, 0);
* thread to unpark is held in successor, which is normally
* just the next node. but if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
node s = node.next;
if (s == null || s.waitstatus > 0) {
s = null;
for (node t = tail; t != null && t != node; t = t.prev)
if (t.waitstatus <= 0)
s = t;
if (s != null)
locksupport.unpark(s.thread);
以上的兩段代碼說明是我覺得比較難了解的,雖然有英文注釋,但是卻沒說明為什麼這麼做,我也是反複調試才想明白。但是不得不說這兩段代碼的巧妙,盡可能利用cas操作減少阻塞的機會,讓線程能有更多機會擷取鎖,畢竟阻塞線程是核心操作,開銷不小。
本篇隻講了普通的lock-unlock,下篇會講講等待-通知,即condition的await-signal的詳細流程
參考資料: