很多人對於Java執行緒的併發都是停留在Lock層面,通過Lock能夠將synchronized粗大的顆粒劃分為很小的顆粒度。然而很多人可能並沒有真正去了解Lock的實現原理。 而不妨一說,AQS就是依靠資料結構的FIFO queue和compareAndSet來現實了強大的併發控制。那麼今天就讓我們一起來“解剖”AQS,從資料結構角度入手,再到具體實現,一覽AQS的全貌。
AQS功能介紹
有些同學可能對AQS不太熟悉,那麼先介紹一下。AQS(AbstractQueuedSynchronizer.class)是ReentrantLock、CountDownLatch等併發工具實現的父類,由AQS來定義了誰能拿到資源、誰需要等待,子類負責搶奪順序的實現。
AQS提供了兩種鎖:獨佔鎖和共享鎖
Node
我們先來看第一個資料結構Node。Node將Thread抽象成Node,同時賦予Node狀態,用不同的狀態來控制Thread的park和unPark。 所以在這一節,你需要了解Node的不同狀態,請看下面摘錄的原始碼。
static final class Node {
/**
* 下面四種都為Node的狀態
* CANCELLED 表明執行緒取消
* SIGNAL 表明成功拿到鎖的執行緒需要喚醒
* CONDITION 表明執行緒按照定義的條件等待
* PROPAGATE 表明下一個acquireShared無條件propagate(在共享鎖中使用)
*/
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// ------------------------------------------------ //
volatile int waitStatus;
volatile Node prev;
volatile Node next;
// 關聯執行緒
volatile Thread thread;
Node nextWaiter;
}
複製程式碼
AbstractQueuedSynchronizer
再來看AbstractQueuedSynchronizer的資料結構,很明顯是一個典型的雙向連結串列,同時使用了state來控制併發。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
*/
private transient volatile Node head;
private transient volatile Node tail;
/**
* 抽象出來的資源
*/
private volatile int state;
}
複製程式碼
ReentrantLock實現 EXCLUSIVE(獨佔鎖)
以ReentrantLock為例,我們來看下如何使用AbstractQueuedSynchronizer。
- Sync實現AbstractQueuedSynchronizer,定義lock為抽象方法,派生出FairSync和NonfairSync。 FairSync.lock加入爭奪佇列,NonfairSync直接去拿資源,如果有執行緒正在使用,才加入佇列
- 呼叫ReentrantLock.lock的時候去呼叫Sync.lock
lock過程
以FairSync為例,lock 等價於 acquire(1);
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
看上面的程式碼,其實分為了兩步:
- tryAcquire(arg) --> FairSync自己實現 // 檢視前面是否有競爭佇列
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg) // 加入佇列,模式是獨佔鎖
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
/**
* 注意這裡自旋去獲取鎖
*/
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
/**
* 會去檢查前面的Node的狀態,當滿足一定條件後才會將執行緒Park住,注意這時沒有跳出迴圈
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
** //Tips: 注意這裡沒有Catch,在併發包很多類中都有這樣的用法,可以google看看**
if (failed)
cancelAcquire(node);
}
}
複製程式碼
unlock 過程
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
複製程式碼
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/**
* 成功後將後面的node轉為unpark
*/
Node s = node.next;
// 存在後面的node有別的狀態的情況,不符合要求則繼續往後找
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);
}
複製程式碼
Tips:建議大家看看LockSupport,注意區分LockSuppork.park和Thread.interrupted概念上的不同。 java執行緒阻塞中斷和LockSupport的常見問題: http://agapple.iteye.com/blog/970055
CountDownLatch實現 SHARD(共享鎖)
說完了獨佔鎖,我們來看看共享鎖,以CountDownLatch為例(CountDownLatch的實現比較簡單便於理解,如果想更好的使用AQS可以看看ReentrantReadWriteLock的實現)。
先說下CountDownLatch使用方式:
- CountDownLatch(int n);
- 工作執行緒呼叫CountDownLatch.countDown(),減少步驟1中的n
- 需要共享的執行緒呼叫CountDownLatch.await,當1中的n為0的時候,執行緒才執行。這裡可以是多個執行緒同時使用await,這裡就是共享鎖的使用場景之一。
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);
}
}
複製程式碼
看完程式碼是不是覺得和獨佔鎖基本一毛一樣。對,除了setHeadAndPropagate(node, r)這個。
setHeadAndPropagate的作用: 當一個shard的Node獲得了資源,那麼就會喚醒佇列中他之後的連續的shard節點,使其同時執行。 這就是共享鎖和獨佔鎖不同之處。
總結一下
AQS提供了子類接觸資源的方式,同時不同的喚醒方式,提供給了使用者獨佔或者共享等不同的鎖的使用方式。 這裡不得不再次感嘆資料結構之美妙,一個簡簡單單的Queue就玩出了獨佔、共享鎖等等花樣。所以,理解原始碼一個很好的方式就是讀懂它內在的資料結構。 折騰不止,學習不止。 望與君共勉~
資料來源
AQS原始碼分析之獨佔鎖和共享鎖: http://blog.csdn.net/luofenghan/article/details/75065001 JDK原始碼AQS: http://childe.net.cn/2017/02/14/JDK%E6%BA%90%E7%A0%81-AQS/