1、概述
談到并發,不得不談ReentrantLock;而談到ReentrantLock,不得不談AbstractQueuedSynchronizer(AQS)!
AQS定義了一個抽象的隊列來進行同步操作,很多同步類都依賴于它,例如常用的ReentrantLock/Semaphore/CountDownLatch等。下面我們來通過源碼分析解開AQS的神秘面紗。
2、框架
它維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。這里volatile是核心關鍵詞,具體volatile的語義,在此不述。state的訪問方式有三種:
- getState()
- setState()
- compareAndSetState()
AQS定義兩種資源共享方式:Exclusive(獨占,只有一個線程能執行,如ReentrantLock)和Share(共享,多個線程可同時執行,如Semaphore/CountDownLatch)。
不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至于具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:
/**
* 獨占式獲取同步狀態,實現該方法需要查詢當前狀態并判斷同步狀態是否符合預期,然后再進行CAS設置同步狀態。
* 成功返回true,失敗返回false。
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
/**
* 獨占式釋放同步狀態,等待獲取同步狀態的線程將有機會獲取同步狀態。
* 成功返回true,失敗返回false。
*/
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
/**
* 共享式獲取同步狀態。
* 1. 返回負數表示失敗。
* 2. 0表示成功,但沒有剩余可用資源。
* 3. 正數表示成功,且有剩余資源。
*/
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
/**
* 共享式釋放同步狀態。
* 如果釋放后允許喚醒后續等待結點返回true,否則返回false。
*/
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
/**
* 當前同步器是否在獨占模式下被線程占用,一般該方法表示是否被當前線程所獨占。
* 只有用到condition才需要去實現它。
*/
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
以ReentrantLock為例,state初始化為0,表示未鎖定狀態。A線程lock()時,會調用tryAcquire()獨占該鎖并將state+1。此后,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)為止,其它線程才有機會獲取該鎖。當然,釋放鎖之前,A線程自己是可以重復獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多么次,這樣才能保證state是能回到零態的。
再以CountDownLatch以例,任務分為N個子線程去執行,state也初始化為N(注意N要與線程個數一致)。這N個子線程是并行執行的,每個子線程執行完后countDown()一次,state會CAS減1。等到所有子線程都執行完后(即state=0),會unpark()主調用線程,然后主調用線程就會從await()函數返回,繼續后余動作。
一般來說,自定義同步器要么是獨占方法,要么是共享方式,但AQS也支持自定義同步器同時實現獨占和共享兩種方式,如ReentrantReadWriteLock。
3、源碼分析
我們先來看看Node元素的類結構圖:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
//表示當前的線程被取消;
static final int CANCELLED = 1;
//表示當前節點的后繼節點包含的線程需要運行,也就是unpark;
static final int SIGNAL = -1;
//表示當前節點在等待condition,也就是在condition隊列中;
static final int CONDITION = -2;
//表示當前場景下后續的acquireShared能夠得以執行;
static final int PROPAGATE = -3;
//表示節點的狀態。默認為0,表示當前節點在sync隊列中,等待著獲取鎖。
//其它幾個狀態為:CANCELLED、SIGNAL、CONDITION、PROPAGATE
volatile int waitStatus;
//前驅節點
volatile Node prev;
//后繼節點
volatile Node next;
//獲取鎖的線程
volatile Thread thread;
//存儲condition隊列中的后繼節點。
Node nextWaiter;
......
}
3.1、acquire(int)
/**
* 獨占獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,
* 將會進入同步隊列等待,該方法會調用重寫的tryAcquire(int arg)方法
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
該方法是獨占模式下線程獲取共享資源的頂層入口。如果獲取到資源,線程直接返回,否則進入等待隊列,直到獲取到資源為止,且整個過程忽略中斷的影響。ReentrantLock的lock方法就是調用的該方法來獲取鎖。
方法的執行流程如下:
- 調用自定義同步器的tryAcquire()嘗試直接去獲取資源,如果成功則直接返回。
- 沒成功,則addWaiter()將該線程加入等待隊列的尾部,并標記為獨占模式。
- acquireQueued()使線程在等待隊列中休息,有機會時(輪到自己,會被unpark())會去嘗試獲取資源。獲取到資源后才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false。
- 如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源后才再進行自我中斷selfInterrupt(),將中斷補上。
可能單看這個流程還是看不太明白,沒關系,接下來我們會一一擊破,等會回過頭來再看這個流程就會非常清晰了。
3.1.1、tryAcquire(int)
上面我們也說過這個方法(具體的資源的獲取/釋放)是需要實現類進行重寫的。至于能不能重入,能不能加鎖,那就看具體的自定義同步器怎么去設計了。當然,自定義同步器在進行資源訪問時要考慮線程安全的影響。
下面有的方法比較簡單,直接看注釋吧。
3.1.2、addWaiter(Node)
/**
* 將當前線程加入到等待隊列的隊尾,并返回當前線程所在的結點
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 首先嘗試在鏈表的后面快速添加節點
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 將該節點添加到隊列尾部
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果首節點為空或者cas添加失敗,則進入enq方法通過自旋方式入隊列(保證成功)
enq(node);
return node;
}
3.1.3、enq(Node)
/**
* 將node加入隊尾
*/
private Node enq(final Node node) {
// 自旋重試
for (;;) {
Node t = tail;
// 當前沒有節點,構造一個new Node(),將head和tail指向它
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 當前有節點,將傳入的Node放在鏈表的最后
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
這里用到了CAS自旋來把Node放到隊尾。
3.1.4、acquireQueued(Node, int)
OK,通過tryAcquire()和addWaiter(),該線程獲取資源失敗,已經被放入等待隊列尾部了。聰明的你立刻應該能想到該線程下一部該干什么了吧:進入等待狀態休息,直到其他線程徹底釋放資源后喚醒自己,自己再拿到資源,然后就可以去干自己想干的事了。沒錯,就是這樣!是不是跟醫院排隊拿號有點相似~~acquireQueued()就是干這件事:在等待隊列中排隊拿號(中間沒其它事干可以休息),直到拿到號后再返回。這個函數非常關鍵,還是上源碼吧
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; // 標記是否成功拿到資源
try {
boolean interrupted = false; // 標記等待過程中是否被中斷過
for (;;) {
final Node p = node.predecessor(); // node的前一個節點
// 如果前一個節點是head,說明當前node節點是第二個節點,接著嘗試去獲取資源
// Note:可能是head釋放完資源喚醒自己的,當然也可能被interrupt了
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted; // 返回等待過程中是否被中斷過
}
// 如果自己可以休息了,就進入waiting狀態,直到被unpark()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; // 如果等待過程中被中斷過,哪怕只有那么一次,就將interrupted標記為true
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/**
* 此方法主要用于檢查狀態,看看自己是否真的可以去休息了
* 1.如果pred的waitStatus是SIGNAL,直接返回true
* 2.如果pred的waitStatus>0,也就是CANCELLED,向前一直找到<=0的節點,讓節點的next指向node
* 3.如果pred的waitStatus<=0,改成SIGNAL
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果已經告訴前驅拿完號后通知自己一下,那就可以安心休息了
return true;
if (ws > 0) {
/*
* 如果前節點放棄了,那就一直往前找,直到找到最近一個正常等待的狀態,并排在它的后邊。
* 注意:那些放棄的結點,由于被自己“加塞”到它們前邊,它們相當于形成一個無引用鏈,
* 稍后就會被保安大叔趕走了(GC回收)!
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前節點正常,那就把前節點的狀態設置成SIGNAL,告訴它拿完號后通知自己一下。有可能失敗,人家說不定剛剛釋放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* 讓線程去休息,真正進入等待狀態
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 調用park()使線程進入waiting狀態
return Thread.interrupted(); // 如果被喚醒,查看是否被中斷(該方法會重置標識位)
}
上面的parkAndCheckInterrupt方法才是真正讓線程“等待”的方法,其中用到了LockSupport的park方法,關于LockSupport可以參考我之前的博文:Java并發之LockSupport
總結一下,acquireQueued總共做了3件事:
- 結點進入隊尾后,檢查狀態,找到安全休息點。
- 調用park()進入waiting狀態,等待unpark()或interrupt()喚醒自己。
- 被喚醒后,看自己是不是有資格能拿到號。如果拿到,head指向當前結點,并返回從入隊到拿到號的整個過程中是否被中斷過;如果沒拿到,繼續流程1。
3.1.5、acquire總結
我們回過頭來再看acquire方法,發現還有一個方法沒有說到,那就是selfInterrupt方法,在
// 重新設置中斷標識位
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
由于此函數是重中之重,最后再用流程圖總結一下:
3.2、release(int)
上一小節已經把acquire()說完了,這一小節就來講講它的反操作release()吧。此方法是獨占模式下線程釋放資源的頂層入口。它會釋放指定量的資源,如果徹底釋放了(即state=0),它會喚醒等待隊列里的其他線程來獲取資源。這也正是unlock()的語義,當然不僅僅只限于unlock()。下面是release()的源碼:
/**
* 釋放資源
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 喚醒等待隊列里的下一個線程
return true;
}
return false;
}
邏輯并不復雜。它調用tryRelease()來釋放資源。有一點需要注意的是,它是根據tryRelease()的返回值來判斷該線程是否已經完成釋放掉資源了!所以自定義同步器在設計tryRelease()的時候要明確這一點!!
3.2.1、tryRelease(int)
跟tryAcquire()一樣,這個方法是需要獨占模式的自定義同步器去實現的。正常來說,tryRelease()都會成功的,因為這是獨占模式,該線程來釋放資源,那么它肯定已經拿到獨占資源了,直接減掉相應量的資源即可(state-=arg),也不需要考慮線程安全的問題。但要注意它的返回值,上面已經提到了,release()是根據tryRelease()的返回值來判斷該線程是否已經完成釋放掉資源了!所以自義定同步器在實現時,如果已經徹底釋放資源(state=0),要返回true,否則返回false。
3.2.2、unparkSuccessor(Node)
此方法用于喚醒等待隊列中下一個線程。下面是源碼:
private void unparkSuccessor(Node node) {
// 這里,node一般為當前線程所在的結點。
int ws = node.waitStatus;
if (ws < 0) // 置零當前線程所在的結點狀態,允許失敗。
compareAndSetWaitStatus(node, ws, 0);
// 找到下一個需要喚醒的結點s
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); // 喚醒
}
3.2.3、release總結
release()是獨占模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果徹底釋放了(即state=0),它會喚醒等待隊列里的其他線程來獲取資源。
3.3、acquireShared(int)
此方法是共享模式下線程獲取共享資源的頂層入口。它會獲取指定量的資源,獲取成功則直接返回,獲取失敗則進入等待隊列,直到獲取到資源為止,整個過程忽略中斷。下面是acquireShared()的源碼:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
這里tryAcquireShared()依然需要自定義同步器去實現。但是AQS已經把其返回值的語義定義好了:負值代表獲取失敗;0代表獲取成功,但沒有剩余資源;正數表示獲取成功,還有剩余資源,其他線程還可以去獲取。
3.3.1、doAcquireShared(int)
此方法用于將當前線程加入等待隊列尾部休息,直到其他線程釋放資源喚醒自己,自己成功拿到相應量的資源后才返回。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED); //加入隊列尾部
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg); // 嘗試獲取資源
if (r >= 0) {
setHeadAndPropagate(node, r); // 將head指向自己,還有剩余資源可以再喚醒之后的線程
p.next = null; // help GC
if (interrupted) // 如果等待過程中被打斷過,此時將中斷補上
selfInterrupt();
failed = false;
return;
}
}
// 判斷狀態,尋找安全點,進入waiting狀態,等著被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
有木有覺得跟acquireQueued()很相似?對,其實流程并沒有太大區別。只不過這里將補中斷的selfInterrupt()放到doAcquireShared()里了,而獨占模式是放到acquireQueued()之外,其實都一樣,不知道Doug Lea是怎么想的。
除此之外,有一個方法很重要:setHeadAndPropagate。它除了重新標記head指向的節點外,還有一個重要的作用,那就是propagate(傳遞),也就是共享的意思。
用圖舉個例子:
因為線程B的讀鎖無法直接獲得鎖,所以需要在Sync隊列中等待,導致后面其他線程的讀鎖都得等待。
當線程A的讀鎖釋放后,線程B的寫鎖獲得鎖,當它釋放后,線程B的讀鎖會獲取到鎖,并傳遞給后面的節點,傳遞的事情就是在setHeadAndPropagate里做的,我們來看看它是如何做的。
3.3.2、setHeadAndPropagate(Node, int)
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;//記錄當前頭節點
//設置新的頭節點,即把當前獲取到鎖的節點設置為頭節點
//注:這里是獲取到鎖之后的操作,不需要并發控制
setHead(node);
//這里意思有兩種情況是需要執行喚醒操作
//1.propagate > 0 表示調用方指明了后繼節點需要被喚醒
//2.頭節點后面的節點需要被喚醒(waitStatus<0),不論是老的頭結點還是新的頭結點
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果當前節點的后繼節點是共享類型或者沒有后繼節點,則進行喚醒
//這里可以理解為除非明確指明不需要喚醒(后繼等待節點是獨占類型),否則都要喚醒
if (s == null || s.isShared())
doReleaseShared();
}
}
此方法在setHead()的基礎上多了一步,就是自己蘇醒的同時,如果條件符合(比如還有剩余資源),還會去喚醒后繼結點,畢竟是共享模式!這樣,形成了一個喚醒鏈,直到寫鎖的節點就停止。
doReleaseShared()我們留著下一小節的releaseShared()里來講。
3.3.3、acquireShared總結
OK,至此,acquireShared()也要告一段落了。讓我們再梳理一下它的流程:
- tryAcquireShared()嘗試獲取資源,成功則直接返回;
其實跟acquire()的流程大同小異,只不過多了個自己拿到資源后,還會去喚醒后繼隊友的操作(這才是共享嘛)。
3.4、releaseShared()
上一小節已經把acquireShared()說完了,這一小節就來講講它的反操作releaseShared()吧。此方法是共享模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果成功釋放且允許喚醒等待線程,它會喚醒等待隊列里的其他線程來獲取資源。下面是releaseShared()的源碼:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {// 嘗試釋放資源
doReleaseShared();// 喚醒后繼結點
return true;
}
return false;
}
此方法的流程也比較簡單,一句話:釋放掉資源后,喚醒后繼。跟獨占模式下的release()相似,但有一點稍微需要注意:獨占模式下的tryRelease()在完全釋放掉資源(state=0)后,才會返回true去喚醒其他線程,這主要是基于獨占下可重入的考量;而共享模式下的releaseShared()則沒有這種要求,共享模式實質就是控制一定量的線程并發執行,那么擁有資源的線程在釋放掉部分資源時就可以喚醒后繼等待結點。例如,資源總量是13,A(5)和B(7)分別獲取到資源并發運行,C(4)來時只剩1個資源就需要等待。A在運行過程中釋放掉2個資源量,然后tryReleaseShared(2)返回true喚醒C,C一看只有3個仍不夠繼續等待;隨后B又釋放2個,tryReleaseShared(2)返回true喚醒C,C一看有5個夠自己用了,然后C就可以跟A和B一起運行。而ReentrantReadWriteLock讀鎖的tryReleaseShared()只有在完全釋放掉資源(state=0)才返回true,所以自定義同步器可以根據需要決定tryReleaseShared()的返回值。
3.4.1、doReleaseShared()
此方法主要用于喚醒后繼。下面是它的源碼:
private void doReleaseShared() {
/*
* 如果head需要通知下一個節點,調用unparkSuccessor
* 如果不需要通知,需要在釋放后把waitStatus改為PROPAGATE來繼續傳播
* 此外,我們必須通過自旋來CAS以防止操作時有新節點加入
* 另外,不同于其他unparkSuccessor的用途,我們需要知道CAS設置狀態失敗的情況,
* 以便進行重新檢查。
*/
for (;;) {
//喚醒操作由頭結點開始,注意這里的頭節點已經是上面新設置的頭結點了
//其實就是喚醒上面新獲取到共享鎖的節點的后繼節點
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//表示后繼節點需要被喚醒
if (ws == Node.SIGNAL) {
//這里需要控制并發,因為入口有setHeadAndPropagate跟release兩個,避免兩次unpark
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//執行喚醒操作
unparkSuccessor(h);
}
//如果后繼節點暫時不需要喚醒,則把當前節點狀態設置為PROPAGATE確保以后可以傳遞下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
//如果頭結點沒有發生變化,表示設置完成,退出循環
//如果頭結點發生變化,比如說其他線程獲取到了鎖,為了使自己的喚醒動作可以傳遞,必須進行重試
if (h == head)
break;
}
}
3.5、Condition
在沒有Lock之前,我們使用synchronized來控制同步,配合Object的wait()、notify()系列方法可以實現等待/通知模式。在Java SE5后,Java提供了Lock接口,相對于Synchronized而言,Lock提供了條件Condition,對線程的等待、喚醒操作更加詳細和靈活。
Condition提供了一系列的方法來對阻塞和喚醒線程:
public interface Condition {
/**
* 造成當前線程在接到信號或被中斷之前一直處于等待狀態。
*/
void await() throws InterruptedException;
/**
* 造成當前線程在接到信號之前一直處于等待狀態。【注意:該方法對中斷不敏感】。
*/
void awaitUninterruptibly();
/**
* 造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態。
* 返回值表示剩余時間,如果在nanosTimesout之前喚醒,那么返回值 = nanosTimeout - 消耗時間,
* 如果返回值 <= 0 ,則可以認定它已經超時了。
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
/**
* 造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態。
*/
boolean await(long time, TimeUnit unit) throws InterruptedException;
/**
* 造成當前線程在接到信號、被中斷或到達指定最后期限之前一直處于等待狀態。
* 如果沒有到指定時間就被通知,則返回true,否則表示到了指定時間,返回返回false。
*/
boolean awaitUntil(Date deadline) throws InterruptedException;
/**
* 喚醒一個等待線程。該線程從等待方法返回前必須獲得與Condition相關的鎖。
*/
void signal();
/**
* 喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition相關的鎖。
*/
void signalAll();
}
Condition是一種廣義上的條件隊列。他為線程提供了一種更為靈活的等待/通知模式,線程在調用await方法后執行掛起操作,直到線程等待的某個條件為真時才會被喚醒。Condition必須要配合鎖一起使用,因為對共享狀態變量的訪問發生在多線程環境下。一個Condition的實例必須與一個Lock綁定,因此Condition一般都是作為Lock的內部實現。
我們這里要說的Condition其實就是AQS的一個內部類:ConditionObject
public class ConditionObject implements Condition, java.io.Serializable {
//頭節點
private transient Node firstWaiter;
//尾節點
private transient Node lastWaiter;
}
每個Condition對象都包含著一個FIFO隊列,該隊列是Condition對象通知/等待功能的關鍵。在隊列中每一個節點都包含著一個線程引用,該線程就是在該Condition對象上等待的線程。Condition擁有首節點(firstWaiter),尾節點(lastWaiter)。當前線程調用await()方法,將會以當前線程構造成一個節點(Node),并將節點加入到該隊列的尾部。
Condition的隊列結構比CLH同步隊列的結構簡單些,新增過程較為簡單只需要將原尾節點的nextWaiter指向新增節點,然后更新lastWaiter即可。
因為大多數方法都差不多,我們這里只重點講解await和signal方法。我們來看看源碼是如何實現的。
3.5.1、await()
調用Condition的await()方法會使當前線程進入等待狀態,同時會加入到Condition等待隊列同時釋放鎖。當從await()方法返回時,當前線程一定是獲取了Condition相關連的鎖。
public final void await() throws InterruptedException {
// 當前線程中斷
if (Thread.interrupted())
throw new InterruptedException();
//當前線程加入等待隊列
Node node = addConditionWaiter();
//釋放鎖,返回釋放之前的狀態
int savedState = fullyRelease(node);
int interruptMode = 0;
/**
* 檢測此節點的線程是否在Sync隊列里,如果不在,則說明該線程還不具備競爭鎖的資格,則繼續等待
* 直到檢測到此節點在Sync隊列里
*/
while (!isOnSyncQueue(node)) {
//線程掛起
LockSupport.park(this);
//如果已經中斷了,則退出循環
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//競爭同步狀態
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//清理下條件隊列中的不是在等待條件的節點
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0) // 對中斷狀態進行判斷,是需要拋異常還是重置中斷位
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
Node t = lastWaiter;
// 檢查隊尾的節點的狀態,清理掉CANCELLED的節點。
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter; // 重新獲取lastWaiter
}
// 新建一個CONDITION節點放到隊尾
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
/**
* 釋放當前狀態值,返回已保存的狀態
*/
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
/**
* 是不是在Sync隊列里
*/
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // 這種情況node一定在Sync隊列中
return true;
// 從隊尾往前找node,找到返回true,否則返回false(這種情況很少發生)
return findNodeFromTail(node);
}
總結一下,await其實就做了幾件事:
- 將當前線程的Condition節點放入等待隊列中。
- 釋放Sync中的鎖。
- 確認節點不在Sync中了,調用park掛起線程。等待signal喚醒。
- 喚醒后競爭鎖。
注:await不會消化中斷異常,如果在await前就interrupt了,在await第一行就會拋出異常。而在await中進行interrupt的話,也會拋出異常。
3.5.2、signal()
調用Condition的signal()方法,將會喚醒在等待隊列中等待最長時間的節點(條件隊列里的首節點),在喚醒節點前,會將節點移到CLH同步隊列中。
public final void signal() {
//檢測當前線程是否為擁有獨占鎖
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//頭節點,喚醒條件隊列中的第一個節點
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
//修改頭結點,完成舊頭結點的移出工作
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && //將節點移動到CLH同步隊列中
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false; // CAS失敗,說明node已經被CANCELLED了
//將節點加入到syn隊列中去,返回的是syn隊列中node節點前面的一個節點
Node p = enq(node);
int ws = p.waitStatus;
//如果結點p的狀態為cancel 或者修改waitStatus失敗,則直接喚醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
可以發現,signal主要是拿到Condition隊列里的第一個節點并調用doSignal方法,主要做了以下幾件事:
- 修改節點waitStatus(把要喚醒的節點waitStatus從CONDITION改為0)。
- 把節點放入syn隊列中(這樣該節點就可以競爭鎖了)。
- 如果節點在Sync隊列的前一個節點狀態是CANCELLED,或者把狀態改為SIGNAL時失敗了,立即喚醒當前節點競爭鎖(如果不這么做,這么節點將永遠不會被喚醒去競爭鎖,導致一直等待)。
signalAll其實就是多次signal。它從Condition隊列的首節點一直遍歷到最后去調用transferForSignal,這里就不講了。
3.6、小結
本節我們詳解了獨占和共享兩種模式下獲取-釋放資源(acquire-release、acquireShared-releaseShared)以及Condition的源碼,相信大家都有一定認識了。
值得注意的是,acquire()和acquireSahred()兩種方法下,線程在等待隊列中都是忽略中斷的。AQS也支持響應中斷的,acquireInterruptibly()/acquireSharedInterruptibly()即是,這里相應的源碼跟acquire()和acquireSahred()差不多,這里就不再詳解了。
4、Mutex(互斥鎖)
Mutex是一個不可重入的互斥鎖實現。鎖資源(AQS里的state)只有兩種狀態:0表示未鎖定,1表示鎖定。下邊是Mutex的核心源碼:
class Mutex implements Lock, java.io.Serializable {
// 自定義同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 判斷是否鎖定狀態
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 嘗試獲取資源,立即返回。成功則返回true,否則false。
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 這里限定只能為1個量
if (compareAndSetState(0, 1)) {//state為0才設置為1,不可重入!
setExclusiveOwnerThread(Thread.currentThread());//設置為當前線程獨占資源
return true;
}
return false;
}
// 嘗試釋放資源,立即返回。成功則為true,否則false。
protected boolean tryRelease(int releases) {
assert releases == 1; // 限定為1個量
if (getState() == 0)//既然來釋放,那肯定就是已占有狀態了。只是為了保險,多層判斷!
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);//釋放資源,放棄占有狀態
return true;
}
}
// 真正同步類的實現都依賴繼承于AQS的自定義同步器!
private final Sync sync = new Sync();
//lock<-->acquire。兩者語義一樣:獲取資源,即便等待,直到成功才返回。
public void lock() {
sync.acquire(1);
}
//tryLock<-->tryAcquire。兩者語義一樣:嘗試獲取資源,要求立即返回。成功則為true,失敗則為false。
public boolean tryLock() {
return sync.tryAcquire(1);
}
//unlock<-->release。兩者語文一樣:釋放資源。
public void unlock() {
sync.release(1);
}
//鎖是否占有狀態
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
同步類在實現時一般都將自定義同步器(sync)定義為內部類,供自己使用;而同步類自己(Mutex)則實現某個接口,對外服務。當然,接口的實現要直接依賴sync,它們在語義上也存在某種對應關系!!而sync只用實現資源state的獲取-釋放方式tryAcquire-tryRelelase,至于線程的排隊、等待、喚醒等,上層的AQS都已經實現好了,我們不用關心。
除了Mutex,ReentrantLock/CountDownLatch/Semphore這些同步類的實現方式都差不多,不同的地方就在獲取-釋放資源的方式tryAcquire-tryRelelase。掌握了這點,AQS的核心便被攻破了!
OK,至此,整個AQS的講解也要落下帷幕了。希望本文能夠對學習Java并發編程的同學有所借鑒,中間寫的有不對的地方,也歡迎討論和指正~