Java并發編程源碼分析系列:
上一篇通過研究ReentrantLock分析了AQS的獨占功能,本文將通過同樣是AQS子類的CountDownLatch分析AQS的共享功能。有了前文研究獨占功能的基礎,再研究共享鎖就簡單多了。
CountDownLatch的使用
CountDownLatch是同步工具類之一,可以指定一個計數值,在并發環境下由線程進行減1操作,當計數值變為0之后,被await方法阻塞的線程將會喚醒,實現線程間的同步。
public void startTestCountDownLatch() {
int threadNum = 10;
final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for (int i = 0; i < threadNum; i++) {
final int finalI = i + 1;
new Thread(() -> {
System.out.println("thread " + finalI + " start");
Random random = new Random();
try {
Thread.sleep(random.nextInt(10000) + 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread " + finalI + " finish");
countDownLatch.countDown();
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadNum + " thread finish");
}
主線程啟動10個子線程后阻塞在await方法,需要等子線程都執行完畢,主線程才能喚醒繼續執行。
構造器
CountDownLatch和ReentrantLock一樣,內部使用Sync繼承AQS。構造函數很簡單地傳遞計數值給Sync,并且設置了state。
Sync(int count) {
setState(count);
}
上文已經介紹過AQS的state,這是一個由子類決定含義的“狀態”。對于ReentrantLock來說,state是線程獲取鎖的次數;對于CountDownLatch來說,則表示計數值的大小。
阻塞線程
接著來看await方法,直接調用了AQS的acquireSharedInterruptibly。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
首先嘗試獲取共享鎖,實現方式和獨占鎖類似,由CountDownLatch實現判斷邏輯。
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
返回1代表獲取成功,返回-1代表獲取失敗。如果獲取失敗,需要調用doAcquireSharedInterruptibly:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
doAcquireSharedInterruptibly的邏輯和獨占功能的acquireQueued基本相同,阻塞線程的過程是一樣的。不同之處:
- 創建的Node是定義成共享的(Node.SHARED);
- 被喚醒后重新嘗試獲取鎖,不只設置自己為head,還需要通知其他等待的線程。(重點看后文釋放操作里的setHeadAndPropagate)
釋放操作
public void countDown() {
sync.releaseShared(1);
}
countDown操作實際就是釋放鎖的操作,每調用一次,計數值減少1:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
同樣是首先嘗試釋放鎖,具體實現在CountDownLatch中:
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
死循環加上cas的方式保證state的減1操作,當計數值等于0,代表所有子線程都執行完畢,被await阻塞的線程可以喚醒了,下一步調用doReleaseShared:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//1
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
//2
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
標記1里,頭節點狀態如果SIGNAL,則狀態重置為0,并調用unparkSuccessor喚醒下個節點。
標記2里,被喚醒的節點狀態會重置成0,在下一次循環中被設置成PROPAGATE狀態,代表狀態要向后傳播。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
在喚醒線程的操作里,分成三步:
- 處理當前節點:非CANCELLED狀態重置為0;
- 尋找下個節點:如果是CANCELLED狀態,說明節點中途溜了,從隊列尾開始尋找排在最前還在等著的節點
- 喚醒:利用LockSupport.unpark喚醒下個節點里的線程。
線程是在doAcquireSharedInterruptibly里被阻塞的,喚醒后調用到setHeadAndPropagate。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
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釋放操作。下個節點被喚醒后,重復上面的步驟,達到共享狀態向后傳播。
要注意,await操作看著好像是獨占操作,但它可以在多個線程中調用。當計數值等于0的時候,調用await的線程都需要知道,所以使用共享鎖。
限定時間的await
CountDownLatch的await方法還有個限定阻塞時間的版本.
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
跟蹤代碼,最后來看doAcquireSharedNanos方法,和上文介紹的doAcquireShared邏輯基本一樣,不同之處是加了time字眼的處理。
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return true;
}
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
進入方法時,算出能夠執行多久的deadline,然后在循環中判斷時間。注意到代碼中間有句:
nanosTimeout > spinForTimeoutThreshold
static final long spinForTimeoutThreshold = 1000L;
spinForTimeoutThreshold寫死了1000ns,這就是所謂的自旋操作。當超時在1000ns內,讓線程在循環中自旋,否則阻塞線程。
總結
兩篇文章分別以ReentrantLock和CountDownLatch為例研究了AQS的獨占功能和共享功能。AQS里的主要方法研究得七七八八了,趁熱打鐵下一篇將會研究其他同步工具類的實現。