天天看點

【Java并發程式設計實戰】—–“J.U.C”:ReentrantLock之二lock方法分析

前一篇部落格簡介了ReentrantLock的定義和與synchronized的差别,以下尾随LZ的筆記來扒扒ReentrantLock的lock方法。我們知道ReentrantLock有公平鎖、非公平鎖之分,是以lock()我也已公平鎖、非公平鎖來進行闡述。首先我們來看ReentrantLock的結構【圖來自Java多線程系列–“JUC鎖”03之 公平鎖(一)】:

【Java并發程式設計實戰】—–“J.U.C”:ReentrantLock之二lock方法分析

從上圖我們能夠看到,ReentrantLock實作Lock接口。Sync與ReentrantLock是組合關系,且FairSync(公平鎖)、NonfairySync(非公平鎖)是Sync的子類。Sync繼承AQS(AbstractQueuedSynchronizer)。在詳細分析lock時。我們須要了解幾個概念:

AQS(AbstractQueuedSynchronizer):為java中管理鎖的抽象類。該類為實作依賴于先進先出 (FIFO) 等待隊列的堵塞鎖和相關同步器(信号量、事件,等等)提供一個架構。該類提供了一個非常重要的機制。在JDK API中是這樣描寫叙述的:為實作依賴于先進先出 (FIFO) 等待隊列的堵塞鎖和相關同步器(信号量、事件。等等)提供一個架構。此類的設計目标是成為依靠單個原子 int 值來表示狀态的大多數同步器的一個實用基礎。子類必須定義更改此狀态的受保護方法。并定義哪種狀态對于此對象意味着被擷取或被釋放。

假定這些條件之後,此類中的其他方法就能夠實作全部排隊和堵塞機制。

子類能夠維護其他狀态字段,但僅僅是為了獲得同步而僅僅追蹤使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法來操作以原子方式更新的 int 值。 這麼長的話用一句話概括就是:維護鎖的目前狀态和線程等待清單。

CLH:AQS中“等待鎖”的線程隊列。

我們知道在多線程環境中我們為了保護資源的安全性常使用鎖将其保護起來,同一時刻僅僅能有一個線程能夠訪問,其餘線程則須要等待,CLH就是管理這些等待鎖的隊列。

CAS(compare and swap):比較并交換函數,它是原子操作函數,也就是說全部通過CAS操作的資料都是以原子方式進行的。

lock()定義例如以下:

lock()内部調用acquire(1),為何是”1”呢?首先我們知道ReentrantLock是獨占鎖,1表示的是鎖的狀态state。對于獨占鎖而言。假設所處于可擷取狀态,其狀态為0,當鎖初次被線程擷取時狀态變成1。

acquire()是AbstractQueuedSynchronizer中的方法。其源代碼例如以下:

從該方法的實作中我們能夠看出,它做了非常多的工作,詳細工作我們先晾着。先看這些方法的實作:

tryAcquire方法是在FairySync中實作的,其源代碼例如以下:

*/

if (c == 0) {

if (!hasQueuedPredecessors() &&

compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

/*

* 假設c != 0,表示該鎖已經被線程占有,則推斷該鎖是否是目前線程占有。若是設定state,否則直接傳回false

else if (current == getExclusiveOwnerThread()) {

int nextc = c + acquires;

if (nextc < 0)

throw new Error("Maximum lock count exceeded");

setState(nextc);

return false;

在這裡我們能夠肯定tryAcquire主要是去嘗試擷取鎖,擷取成功則設定鎖狀态并傳回true。否則傳回false。

hasQueuedPredecessors:”目前線程”是不是在CLH隊列的隊首。來傳回AQS中是不是有比“目前線程”等待更久的線程(公平鎖)。

Node是AbstractQueuedSynchronizer的内部類。它代表着CLH清單的一個線程節點。對于Node以後LZ會詳細闡述的。

compareAndSetState:設定鎖狀态

compareAndSwapInt() 是sun.misc.Unsafe類中的一個本地方法。

對此,我們須要了解的是 compareAndSetState(expect, update) 是以原子的方式操作目前線程。若目前線程的狀态為expect。則設定它的狀态為update。

setExclusiveOwnerThread:設定目前線程為鎖的擁有者

addWaiter()主要是将目前線程增加到CLH隊列隊尾。

當中compareAndSetTail和enq的源代碼例如以下:

否則,直接将node增加到CLH末尾 * @param node

* @return

private Node enq(final Node node) {

for (;;) {

Node t = tail;

if (t == null) {

if (compareAndSetHead(new Node()))

tail = head;

} else {

node.prev = t;

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

addWaiter的實作比較簡單且實作功能明了:目前線程增加到CLH隊列隊尾。

這是非公平鎖和公平鎖的一個重要差别。 */

if (p == head && tryAcquire(arg)) {

setHead(node); //将目前節點設定設定為頭結點

p.next = null;

failed = false;

return interrupted;

/* 假設不是head直接後繼或擷取鎖失敗。則檢查是否要堵塞目前線程,是則堵塞目前線程

* shouldParkAfterFailedAcquire:推斷“目前線程”是否須要堵塞

* parkAndCheckInterrupt:堵塞目前線程

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

interrupted = true;

} finally {

if (failed)

cancelAcquire(node);

在這個for循環中。LZ不是非常明确為什麼要加p==head,Java多線程系列–“JUC鎖”03之 公平鎖(一)這篇部落格有一個較好的解釋例如以下:

p == head && tryAcquire(arg) 

首先,推斷“前繼節點”是不是CHL表頭。假設是的話,則通過tryAcquire()嘗試擷取鎖。 

事實上,這樣做的目的是為了“讓目前線程擷取鎖”,可是為什麼須要先推斷p==head呢?了解這個對了解“公平鎖”的機制非常重要。由于這麼做的原因就是為了保證公平性! 

      (a) 前面,我們在shouldParkAfterFailedAcquire()我們推斷“目前線程”是否須要堵塞; 

      (b) 接着。“目前線程”堵塞的話。會調用parkAndCheckInterrupt()來堵塞線程。當線程被解除堵塞的時候,我們會傳回線程的中斷狀态。而線程被解決堵塞,可能是由于“線程被中斷”,也可能是由于“其他線程調用了該線程的unpark()函數”。 

      (c) 再回到p==head這裡。

假設目前線程是由于其他線程調用了unpark()函數而被喚醒,那麼喚醒它的線程。應該是它的前繼節點所相應的線程(關于這一點,後面在“釋放鎖”的過程中會看到)。

OK,是前繼節點調用unpark()喚醒了目前線程!

此時,再來了解p==head就非常easy了:目前繼節點是CLH隊列的頭節點,而且它釋放鎖之後。就輪到目前節點擷取鎖了。然後。目前節點通過tryAcquire()擷取鎖。擷取成功的話,通過setHead(node)設定目前節點為頭節點。并傳回。

       總之,假設“前繼節點調用unpark()喚醒了目前線程”而且“前繼節點是CLH表頭”。此時就是滿足p==head,也就是符合公平性原則的。否則,假設目前線程是由于“線程被中斷”而喚醒,那麼顯然就不是公平了。這就是為什麼說p==head就是保證公平性!

在該方法中有兩個方法比較重要。shouldParkAfterFailedAcquire和parkAndCheckInterrupt。當中

shouldParkAfterFailedAcquire:推斷“目前線程”是否須要堵塞,源代碼例如以下:

waitStatus是節點Node定義的,她是辨別線程的等待狀态。他主要有例如以下四個值:

CANCELLED = 1:線程已被取消;

SIGNAL = -1:目前線程的後繼線程須要被unpark(喚醒);

CONDITION = -2 :線程(處在Condition休眠狀态)在等待Condition喚醒;

PROPAGATE = –3:(共享鎖)其他線程擷取到“共享鎖”.

有了這四個狀态,我們再來分析上面代碼,當ws == SIGNAL時表明目前節點須要unpark(喚醒),直接傳回true,當ws > 0 (CANCELLED),表明目前節點已經被取消了。則通過回溯的方法(do{}while())向前找到一個非CANCELLED的節點并傳回false。其他情況則設定該節點為SIGNAL狀态。我們再回到if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())。p是目前節點的前繼節點。當該前繼節點狀态為SIGNAL時傳回true。表示目前線程須要堵塞。則調用parkAndCheckInterrupt()堵塞目前線程。

parkAndCheckInterrupt:堵塞目前線程,而且傳回“線程被喚醒之後”的中斷狀态,源代碼例如以下:

從上面我們能夠總結,acquireQueued()是目前線程會依據公平性原則來進行堵塞等待,直到擷取鎖為止。而且傳回目前線程在等待過程中有沒有并中斷過。

selfInterrupt()産生一個中斷。假設在acquireQueued()中目前線程被中斷過。則須要産生一個中斷。

我們再看acquire()源代碼:

首先通過tryAcquire方法嘗試擷取鎖,假設成功直接傳回。否則通過acquireQueued()再次擷取。在acquireQueued()中會先通過addWaiter将目前線程增加到CLH隊列的隊尾,在CLH隊列中等待。在等待過程中線程處于休眠狀态,直到成功擷取鎖才會傳回。例如以下:

【Java并發程式設計實戰】—–“J.U.C”:ReentrantLock之二lock方法分析

非公平鎖NonfairSync的lock()與公平鎖的lock()在擷取鎖的流程上是一直的,可是由于它是非公平的,是以擷取鎖機制還是有點不同。通過前面我們了解到公平鎖在擷取鎖時採用的是公平政策(CLH隊列),而非公平鎖則採用非公平政策它無視等待隊列,直接嘗試擷取。

例如以下:

lock()通過compareAndSetState嘗試設定所狀态,若成功直接将鎖的擁有者設定為目前線程(簡單粗暴),否則調用acquire()嘗試擷取鎖;

在非公平鎖中acquire()的實作和公平鎖一模一樣,可是他們嘗試擷取鎖的機制不同(也就是tryAcquire()的實作不同)。

tryAcquire内部調用nonfairyTryAcquire:

與公平鎖相比,非公平鎖的不同之處就體如今if(c==0)的條件代碼塊中:

是否已經發現了不同之處。公平鎖中要通過hasQueuedPredecessors()來推斷該線程是否位于CLH隊列中頭部,是則擷取鎖;而非公平鎖則無論你在哪個位置都直接擷取鎖。

繼續閱讀