Lock.lock/unlock
接上篇,這篇從Lock.lock/unlock開始葵袭。特別說明在沒有特殊情況下所有程序尸饺、API、文檔都是基于JDK 6.0的沈堡。
public void java.util.concurrent.locks.ReentrantLock.lock()
獲取鎖静陈。
如果該鎖沒有被另一個線程保持,則獲取該鎖并立即返回诞丽,將鎖的保持計數(shù)設(shè)置為 1鲸拥。
如果當前線程已經(jīng)保持該鎖,則將保持計數(shù)加 1僧免,并且該方法立即返回刑赶。
如果該鎖被另一個線程保持,則出于線程調(diào)度的目的懂衩,禁用當前線程撞叨,并且在獲得鎖之前,該線程將一直處于休眠狀態(tài)浊洞,此時鎖保持計數(shù)被設(shè)置為 1牵敷。
從上面的文檔可以看出ReentrantLock是可重入鎖的實現(xiàn)。而內(nèi)部是委托java.util.concurrent.locks.ReentrantLock.Sync.lock()實現(xiàn)的法希。java.util.concurrent.locks.ReentrantLock.Sync是抽象類枷餐,有java.util.concurrent.locks.ReentrantLock.FairSync和java.util.concurrent.locks.ReentrantLock.NonfairSync兩個實現(xiàn),也就是常說的公平鎖和不公平鎖苫亦。
公平鎖和非公平鎖
如果獲取一個鎖是按照請求的順序得到的毛肋,那么就是公平鎖奕锌,否則就是非公平鎖。
在沒有深入了解內(nèi)部機制及實現(xiàn)之前村生,先了解下為什么會存在公平鎖和非公平鎖惊暴。公平鎖保證一個阻塞的線程最終能夠獲得鎖,因為是有序的趁桃,所以總是可以按照請求的順序獲得鎖辽话。不公平鎖意味著后請求鎖的線程可能在其前面排列的休眠線程恢復前拿到鎖,這樣就有可能提高并發(fā)的性能卫病。這是因為通常情況下掛起的線程重新開始與它真正開始運行油啤,二者之間會產(chǎn)生嚴重的延時。因此非公平鎖就可以利用這段時間完成操作蟀苛。這是非公平鎖在某些時候比公平鎖性能要好的原因之一益咬。
二者在實現(xiàn)上的區(qū)別會在后面介紹,我們先從公平鎖(FairSync)開始帜平。
前面說過java.util.concurrent.locks.AbstractQueuedSynchronizer (AQS)是Lock的基礎(chǔ)幽告,對于一個FairSync而言,lock()就直接調(diào)用AQS的acquire(int arg);
public final void acquire(int arg) 以獨占模式獲取對象裆甩,忽略中斷冗锁。通過至少調(diào)用一次
*tryAcquire(int)*
來實現(xiàn)此方法,并在成功時返回嗤栓。否則在成功之前冻河,一直調(diào)用*tryAcquire(int)*
將線程加入隊列,線程可能重復被阻塞或不被阻塞茉帅。
在介紹實現(xiàn)之前先要補充上一節(jié)的知識叨叙,對于一個AQS的實現(xiàn)而言,通常情況下需要實現(xiàn)以下方法來描述如何鎖定線程堪澎。
**tryAcquire(int)**
試圖在獨占模式下獲取對象狀態(tài)擂错。此方法應該查詢是否允許它在獨占模式下獲取對象狀態(tài),如果允許全封,則獲取它马昙。此方法總是由執(zhí)行 acquire 的線程來調(diào)用。如果此方法報告失敗刹悴,則 acquire 方法可以將線程加入隊列(如果還沒有將它加入隊列),直到獲得其他某個線程釋放了該線程的信號攒暇。也就是說此方法是一種嘗試性方法土匀,如果成功獲取鎖那最好,如果沒有成功也沒有關(guān)系形用,直接返回false就轧。
**tryRelease(int)**
試圖設(shè)置狀態(tài)來反映獨占模式下的一個釋放证杭。 此方法總是由正在執(zhí)行釋放的線程調(diào)用。釋放鎖可能失敗或者拋出異常妒御,這個在后面會具體分析解愤。
**tryAcquireShared(int)** 試圖在共享模式下獲取對象狀態(tài)。
**tryReleaseShared(int)** 試圖設(shè)置狀態(tài)來反映共享模式下的一個釋放乎莉。
**isHeldExclusively()** 如果對于當前(正調(diào)用的)線程送讲,同步是以獨占方式進行的,則返回 true惋啃。
除了tryAcquire(int)外哼鬓,其它方法會在后面具體介紹。首先對于ReentrantLock而言边灭,不管是公平鎖還是非公平鎖异希,都是獨占鎖,也就是說同時能夠有一個線程持有鎖绒瘦。因此對于acquire(int arg)而言称簿,arg==1。在AQS中acquire的實現(xiàn)如下:
public final void acquire(int arg) {
? if (!tryAcquire(arg) &&
? acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
? selfInterrupt();
}
這個看起來比較復雜惰帽,我們分解以下4個步驟予跌。
- 如果tryAcquire(arg)成功,那就沒有問題善茎,已經(jīng)拿到鎖券册,整個lock()過程就結(jié)束了。如果失敗進行操作2垂涯。
- 創(chuàng)建一個獨占節(jié)點(Node)并且此節(jié)點加入CHL隊列末尾烁焙。進行操作3。
- 自旋嘗試獲取鎖耕赘,失敗根據(jù)前一個節(jié)點來決定是否掛起(park())骄蝇,直到成功獲取到鎖。進行操作4操骡。
- 如果當前線程已經(jīng)中斷過九火,那么就中斷當前線程(清除中斷位)。
這是一個比較復雜的過程册招,我們按部就班一個一個分析岔激。
tryAcquire(acquires)
對于公平鎖而言,它的實現(xiàn)方式如下:
? protected final boolean tryAcquire(int acquires) {
? final Thread current = Thread.currentThread();
? int c = getState();
? if (c == 0) {
? if (isFirst(current) &&
? 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;
? }
}
在這段代碼中是掰,前面說明對于AQS存在一個state來描述當前有多少線程持有鎖虑鼎。由于AQS支持共享鎖(例如讀寫鎖,后面會繼續(xù)講),所以這里state>=0炫彩,但是由于ReentrantLock是獨占鎖匾七,所以這里不妨理解為0<=state,acquires=1江兢。isFirst(current)是一個很復雜的邏輯昨忆,包括踢出無用的節(jié)點等復雜過程,這里暫且不提杉允,大體上的意思是說判斷AQS是否為空或者當前線程是否在隊列頭(為了區(qū)分公平與非公平鎖)邑贴。
- 如果當前鎖有其它線程持有,c!=0夺颤,進行操作2痢缎。否則,如果當前線程在AQS隊列頭部世澜,則嘗試將AQS狀態(tài)state設(shè)為acquires(等于1)独旷,成功后將AQS獨占線程設(shè)為當前線程返回true,否則進行2寥裂。這里可以看到compareAndSetState就是使用了CAS操作嵌洼。
- 判斷當前線程與AQS的獨占線程是否相同,如果相同封恰,那么就將當前狀態(tài)位加1(這里+1后結(jié)果為負數(shù)后面會講麻养,這里暫且不理它),修改狀態(tài)位诺舔,返回true鳖昌,否則進行3。這里之所以不是將當前狀態(tài)位設(shè)置為1低飒,而是修改為舊值+1呢许昨?這是因為ReentrantLock是可重入鎖,同一個線程每持有一次就+1褥赊。
- 返回false糕档。
比較非公平鎖的tryAcquire實現(xiàn)java.util.concurrent.locks.ReentrantLock.Sync.nonfairTryAcquire(int),公平鎖多了一個判斷當前節(jié)點是否在隊列頭拌喉,這個就保證了是否按照請求鎖的順序來決定獲取鎖的順序(同一個線程的多次獲取鎖除外)速那。
現(xiàn)在再回頭看公平鎖和非公平鎖的lock()方法。公平鎖只有一句acquire(1)尿背;而非公平鎖的調(diào)用如下:
final void lock() {
? if (compareAndSetState(0, 1))
? setExclusiveOwnerThread(Thread.currentThread());
? else
? acquire(1);
}
很顯然端仰,非公平鎖在第一次獲取鎖,或者其它線程釋放鎖后(可能等待)残家,優(yōu)先采用compareAndSetState(0,1)然后設(shè)置AQS獨占線程而持有鎖榆俺,這樣有時候比acquire(1)順序檢查鎖持有而要高效。即使在重入鎖上坞淮,也就是compareAndSetState(0,1)失敗茴晋,但是是當前線程持有鎖上,非公平鎖也沒有問題回窘。
addWaiter(mode)
tryAcquire失敗就意味著入隊列了诺擅。此時AQS的隊列中節(jié)點Node就開始發(fā)揮作用了。一般情況下AQS支持獨占鎖和共享鎖啡直,而獨占鎖在Node中就意味著條件(Condition)隊列為空(上一篇中介紹過相關(guān)概念)烁涌。在java.util.concurrent.locks.AbstractQueuedSynchronizer.Node中有兩個常量,
static final Node EXCLUSIVE = null; //獨占節(jié)點模式
static final Node SHARED = new Node(); //共享節(jié)點模式
addWaiter(mode)中的mode就是節(jié)點模式酒觅,也就是共享鎖還是獨占鎖模式撮执。
前面一再強調(diào)ReentrantLock是獨占鎖模式。
private Node addWaiter(Node mode) {
? Node node = new Node(Thread.currentThread(), mode);
? // Try the fast path of enq; backup to full enq on failure
? Node pred = tail;
? if (pred != null) {
? node.prev = pred;
? if (compareAndSetTail(pred, node)) {
? pred.next = node;
? return node;
? }
? }
? enq(node);
? return node;
}
上面是節(jié)點如隊列的一部分舷丹。當前僅當隊列不為空并且將新節(jié)點插入尾部成功后直接返回新節(jié)點抒钱。否則進入enq(Node)進行操作。
private Node enq(final Node node) {
? for (;;) {
? Node t = tail;
? if (t == null) { // Must initialize
? Node h = new Node(); // Dummy header
? h.next = node;
? node.prev = h;
? if (compareAndSetHead(h)) {
? tail = node;
? return h;
? }
? }
? else {
? node.prev = t;
? if (compareAndSetTail(t, node)) {
? t.next = node;
? return t;
? }
? }
? }
}
enq(Node)去隊列操作實現(xiàn)了CHL隊列的算法颜凯,如果為空就創(chuàng)建頭結(jié)點谋币,然后同時比較節(jié)點尾部是否是改變來決定CAS操作是否成功,當且僅當成功后才將為不節(jié)點的下一個節(jié)點指向為新節(jié)點症概±俣睿可以看到這里仍然是CAS操作。
acquireQueued(node,arg)
自旋請求鎖彼城,如果可能的話掛起線程诅蝶,直到得到鎖,返回當前線程是否中斷過(如果park()過并且中斷過的話有一個interrupted中斷位)募壕。
final boolean acquireQueued(final Node node, int arg) {
? try {
? boolean interrupted = false;
? for (;;) {
? final Node p = node.predecessor();
? if (p == head && tryAcquire(arg)) {
? setHead(node);
? p.next = null; // help GC
? return interrupted;
? }
? if (shouldParkAfterFailedAcquire(p, node) &&
? parkAndCheckInterrupt())
? interrupted = true;
? }
? } catch (RuntimeException ex) {
? cancelAcquire(node);
? throw ex;
? }
}
下面的分析就需要用到上節(jié)節(jié)點的狀態(tài)描述了调炬。acquireQueued過程是這樣的:
- 如果當前節(jié)點是AQS隊列的頭結(jié)點(如果第一個節(jié)點是DUMP節(jié)點也就是傀儡節(jié)點,那么第二個節(jié)點實際上就是頭結(jié)點了)司抱,就嘗試在此獲取鎖tryAcquire(arg)筐眷。如果成功就將頭結(jié)點設(shè)置為當前節(jié)點(不管第一個結(jié)點是否是DUMP節(jié)點),返回中斷位习柠。否則進行2匀谣。
- 檢測當前節(jié)點是否應該park(),如果應該park()就掛起當前線程并且返回當前線程中斷位资溃。進行操作1武翎。
一個節(jié)點是否該park()是關(guān)鍵,這是由方法java.util.concurrent.locks.AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire(Node, Node)實現(xiàn)的溶锭。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
? int s = pred.waitStatus;
? if (s < 0) return true;
? if (s > 0) {
? do {
? node.prev = pred = pred.prev;
? } while (pred.waitStatus > 0);
? pred.next = node;
? } else compareAndSetWaitStatus(pred, 0, Node.SIGNAL);
? return false;
}
- 如果前一個節(jié)點的等待狀態(tài)waitStatus<0宝恶,也就是前面的節(jié)點還沒有獲得到鎖,那么返回true,表示當前節(jié)點(線程)就應該park()了垫毙。否則進行2霹疫。
- 如果前一個節(jié)點的等待狀態(tài)waitStatus>0,也就是前一個節(jié)點被CANCELLED了综芥,那么就將前一個節(jié)點去掉丽蝎,遞歸此操作直到所有前一個節(jié)點的waitStatus<=0,進行4膀藐。否則進行3屠阻。
- 前一個節(jié)點等待狀態(tài)waitStatus=0,修改前一個節(jié)點狀態(tài)位為SINGAL额各,表示后面有節(jié)點等待你處理国觉,需要根據(jù)它的等待狀態(tài)來決定是否該park()。進行4虾啦。
- 返回false麻诀,表示線程不應該park()。
selfInterrupt()
private static void selfInterrupt() {
? Thread.currentThread().interrupt();
}
如果線程曾經(jīng)中斷過(或者阻塞過)(比如手動interrupt()或者超時等等缸逃,那么就再中斷一次针饥,中斷兩次的意思就是清除中斷位)。
大體上整個Lock.lock()就這樣一個流程需频。除了lock()方法外丁眼,還有l(wèi)ockInterruptibly()/tryLock()/unlock()/newCondition()等,在接下來的章節(jié)中會一一介紹昭殉。