AQS就是AbstractQueuedSynchronizer,它是爲實現依賴於先進先出(FIFO)等待隊列的阻塞鎖和相關同步器(信號量,事件等)提供的一個框架。AQS繼承了AbstractOwnableSynchronizer類,這個類爲創建鎖和相關同步器提供了基礎。AQS是Concurrent包的核心,lock就是在AQS的基礎上實現的,阻塞隊列,線程池,信號量等都離不開AQS的支持。
CLH
CLH lock queue通常被用來處理併發的情況,它通過雙向隊列(FIFO)來完成同步狀態。每個線程都會被封裝成一個Node節點放到同步隊列中。
每個Node節點保存了當前線程的同步狀態,等待狀態,前驅和後繼節點等。
static final class Node {
// 共享模式下等待的標記
static final Node SHARED = new Node();
// 獨佔模式下等待的標記
static final Node EXCLUSIVE = null;
// 線程的等待狀態 表示線程已經被取消
static final int CANCELLED = 1;
// 線程的等待狀態 表示後繼線程需要被喚醒
static final int SIGNAL = -1;
// 線程的等待狀態 表示線程在Condtion上
static final int CONDITION = -2;
// 表示下一個acquireShared需要無條件的傳播
static final int PROPAGATE = -3;
/**
* SIGNAL: 當前節點的後繼節點處於等待狀態時,如果當前節點的同步狀態被釋放或者取消,
* 必須喚起它的後繼節點
*
* CANCELLED: 一個節點由於超時或者中斷需要在CLH隊列中取消等待狀態,被取消的節點不會再次等待
*
* CONDITION: 當前節點在等待隊列中,只有當節點的狀態設爲0的時候該節點纔會被轉移到同步隊列
*
* PROPAGATE: 下一次的共享模式同步狀態的獲取將會無條件的傳播
* waitStatus的初始值時0,使用CAS來修改節點的狀態
*/
volatile int waitStatus;
/**
* 當前節點的前驅節點,當前線程依賴它來檢查waitStatus,在入隊的時候才被分配,
* 並且只在出隊的時候才被取消(爲了GC),頭節點永遠不會被取消,一個節點成爲頭節點
* 僅僅是成功獲取到鎖的結果,一個被取消的線程永遠也不會獲取到鎖,線程只取消自身,
* 而不涉及其他節點
*/
volatile Node prev;
/**
* 當前節點的後繼節點,當前線程釋放的才被喚起,在入隊時分配,在繞過被取消的前驅節點
* 時調整,在出隊列的時候取消(爲了GC)
* 如果一個節點的next爲空,我們可以從尾部掃描它的prev,雙重檢查
* 被取消節點的next設置爲指向節點本身而不是null,爲了isOnSyncQueue更容易操作
*/
volatile Node next;
/**
* 當前節點的線程,初始化後使用,在使用後失效
*/
volatile Thread thread;
/**
* 鏈接到下一個節點的等待條件,或特殊的值SHARED,因爲條件隊列只有在獨佔模式時才能被訪問,
* 所以我們只需要一個簡單的連接隊列在等待的時候保存節點,然後把它們轉移到隊列中重新獲取
* 因爲條件只能是獨佔性的,我們通過使用特殊的值來表示共享模式
*/
Node nextWaiter;
/**
* 如果節點處於共享模式下等待直接返回true
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回當前節點的前驅節點,如果爲空,直接拋出空指針異常
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // 用來建立初始化的head 或 SHARED的標記
}
Node(Thread thread, Node mode) { // 指定線程和模式的構造方法
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 指定線程和節點狀態的構造方法
this.waitStatus = waitStatus;
this.thread = thread;
}
}
AQS具有頭節點和尾節點
private transient volatile Node head;
private transient volatile Node tail;
Node是構成同步隊列的基礎,看一下Node的結構
同步隊列中首節點是獲取到鎖的節點,它在釋放的時候會喚醒後繼節點,後繼節點獲取到鎖的時候,會把自己設爲首節點。
注意:設置首節點不需要使用CAS,因爲在併發環境中只有一個線程能獲取到鎖,只有獲取到鎖的線程才能設置首節點。
獨佔式
-
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
通過調用acquire的方法獲取同步狀
態
,
該方法
忽略中斷
,
線程獲取同步狀態失敗後
,
進入同步隊列
,
在對其進行中斷操作後
,
線程不會從同步隊列移除。
首先調用tryAcquire方法獲取同步狀態
,
AQS並沒有實現這個方法
,
具體的實現由它的繼承類進行重寫
,
比如ReentrantLock的Sync類。如果獲取同步狀態成功直接返回true
;
如果獲取同步狀態失敗的話
,
調用addWaiter方法把線程封裝成一個Node節點添加到同步隊列的尾部
,
最後調用acquireQueued方法使節點以自旋的方式獲取同步狀態
,
如果獲取同步狀態失敗
,
要掛起線程
,
最後
,
線程如果在獲取同步狀態中和同步隊列中被中斷過
,
要進行自我中斷。
看一下addWaiter方法
/**
* 把Node節點添加到同步隊列的尾部
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 以獨佔模式把當前線程封裝成一個Node節點
// 嘗試快速入隊
Node pred = tail; // 當前隊列的尾節點賦給pred
if (pred != null) { // 先覺條件 尾節點不爲空
node.prev = pred; // 把pred作爲node的前繼節點
if (compareAndSetTail(pred, node)) { //利用CAS把node作爲尾節點
pred.next = node; // 把node作爲pred的後繼節點
return node; // 直接返回node
}
}
enq(node); // 尾節點爲空或者利用CAS把node設爲尾節點失敗
return node;
}
/**
* 採用自旋的方式把node插入到隊列中
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 如果t爲空,說明隊列爲空,必須初始化
if (compareAndSetHead(new Node())) // 新建一個節點利用CAS設爲頭節點,就是這樣的形式 head=tail=null
tail = head;
} else { // 尾節點不爲空的情況
node.prev = t; // 把t設爲node的前驅節點
if (compareAndSetTail(t, node)) { // 利用CAS把node節點設爲尾節點
t.next = node; // 更改指針 把node作爲t的後繼節點
return t; // 直接返回t
}
}
}
}
enq方法中採用了非常經典的自旋操作,只有通過CAS把node設爲尾節點後,當前線程才能退出該方法,否則的話,當前線程不斷的嘗試,直到能把節點添加到隊列中爲止,這樣就把並行添加變成了串行添加。
繼續看一下acquireQueued方法
/*
* 此主要是通過自旋方式獲取同步狀態
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false; // 默認線程沒有被中斷過
for (;;) {
final Node p = node.predecessor(); // 獲取該節點的前驅節點p
if (p == head && tryAcquire(arg)) { // 如果p是頭節點並且能獲取到同步狀態
setHead(node); // 把當前節點設爲頭節點
p.next = null; // 把p的next設爲null,便於GC
failed = false; // 標誌--表示成功獲取同步狀態,默認是true,表示失敗
return interrupted; // 返回該線程在獲取到同步狀態的過程中有沒有被中斷過
}
if (shouldParkAfterFailedAcquire(p, node) && // 用於判斷是否掛起當前線程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) // 如果fail爲true,直接移除當前節點
cancelAcquire(node);
}
}
這個方法比較複雜,裏面包含了很多其他的方法,我們先看獲取當前節點的前驅節點,如果前驅節點是頭節點,有兩種情況,一種是默認空的頭節點,說明此時是同步隊列中的第一個線程去嘗試獲取同步狀態,另一種是獲取到同步狀態的節點,然後再一次調用子類重寫的tryAcquire方法去獲取同步狀態,如果成功獲取同步狀態,則把當前節點設爲頭節點。如果當前節點的前驅節點不是頭節點或者沒有獲取到同步狀態的話,就要調用shouldParkAfterFailedAcquire方法掛起當前線程。
繼續看shouldParkAfterFailedAcquire方法
/**
* 如果線程獲取同步狀態失敗就要檢查它的節點status,要保證prev = node.prev
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 獲取當前節點的前驅節點的waitStatus
if (ws == Node.SIGNAL)
/*
* 如果前驅節點的ws = singal,表示前驅節點釋放後會喚起當前線程,
* 可以安全的掛起當前線程
*/
return true; // 能夠掛起當前線程直接返回true
if (ws > 0) {
/*
* 前驅節點的ws > 0,說明ws = Cancelled,表示前驅線程被取消,
* 從前驅節點繼續往前遍歷,直到找到第一個前驅節點的ws <= 0 爲止
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 這種情況表示前驅節點的 ws = 0 或者 ws = PROPAGATE,我們需要一個singal,但是
* 不能掛起當前線程
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
這段代碼用來檢測是否掛起當前線程,分三種情況,第一種情況是前驅節點的 ws = singal,表示前驅節點釋放同步狀態的時候會喚醒當前節點,可以安全掛起當前線程;第二種情況是前驅節點被取消,那就從前驅節點繼續往前遍歷,直到往前找到第一個ws <= 0 的節點;第三種是前驅節點的 ws = 0,表示前驅節點獲取到同步狀態,當前線程不能掛起,應該嘗試去獲取同步狀態,前驅節點的同步狀態的釋放正好可以讓當前節點進行獲取,所以使用CAS把前驅節點的ws設爲singal,另外如果 ws =PROPAGATE,說明正以共享模式進行傳播,也需要使用CAS把ws設爲singal。
在shouldParkAfterFailedAcquire返回true的情況下,繼續看parkAndCheckInterrupted方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
調用LockSupport的park方法掛起當前線程,返回該線程是否被中斷過,如果被中斷過,直接設置interrupted = true。
如果獲取同步狀態失敗,採用cancelAcquire方法取消當前節點
/**
* 取消當前節點
*/
private void cancelAcquire(Node node) {
// 當前節點不存在的話直接忽略
if (node == null)
return;
node.thread = null; // 把當前節點的線程設爲null
// 獲取當前節點的前驅pred
Node pred = node.prev;
while (pred.waitStatus > 0) // 如果prde的ws > 0,直接跳過pred繼續往前遍歷,直到pred的
node.prev = pred = pred.prev; // ws <= 0
// 獲取pred的後繼predNext
Node predNext = pred.next;
// 把node節點的ws設爲CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果node是尾節點,利用CAS把pred設爲尾節點,predNext爲null
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// pred不是頭結點 && pred的線程不爲空 && pred.ws = singal
// 利用CAS把node的next設爲pred的next節點
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else { // node是頭結點,喚起它的後繼節點
unparkSuccessor(node);
}
node.next = node; // node指向自己,便於GC
}
}
分三種情況進行考慮:
1。 node本身就是尾節點,直接把node的prev設爲尾節點
2。 node的prev不是頭結點,直接把prev和node的next進行連接
3。 node的prev是頭結點,使用unparkSuccessor喚醒後繼節點
看一下unparkSuccessor方法
/**
* 如果node存在喚醒它的後繼節點
*/
private void unparkSuccessor(Node node) {
/*
* 獲取node的ws,如果ws<0,使用CAS把node的ws設爲0,表示釋放同步狀態
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* 獲取node的後繼節點s,根據條件s = null 或者 s.ws > 0,從同步隊列的尾部開始遍歷,
* 直到找到距node最近的滿足ws <= 0的節點t,把t賦給s,喚醒s節點的線程
* 如果s不爲null && s的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);
}
這就是整個acquireQueued的流程,如果執行完acquireQueued方法返回線程被中斷過,那線程最後要進行自我中斷一下
/**
* 當前線程的自我中斷
*/
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
下面看一下同步狀態的釋放
/**
* 以獨佔模式釋放同步狀態,當前線程釋放同步狀態的時候,會喚醒同步隊列上的後繼節點
* 釋放成功後之後直接返回true
*/
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方法,這個方法在AQS中直接拋出異常,必須由繼承它的子類去重寫此方法,如果此方法返回成功釋放同步狀態,如果當前節點不是空並且ws!=0,直接調用unparkSuccessor方法喚醒當前節點的後繼節點。
獲取同步狀態的其他方法:
1、 響應式中斷獲取同步狀態
/**
* 當前線程被中斷後,直接拋出異常,否則的話,再次調用tryAcquire方法獲取同步狀態
*/
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
這個方法和acquire方法很相似,只不過線程在被中斷後直接拋出異常。
2、 指定時間內獲取同步狀態
/**
* 以獨佔模式獲取同步狀態,線程被中斷,直接拋出異常,如果在指定時間內沒有獲取到同步狀態,
* 直接返回false,表現獲取同步狀態失敗.
*/
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
主要看一下doAcquireNanos方法
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
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);
}
}
如果指定時間nanosTimeOut <= 0,表示已經超時,直接返回false;如果當前節點可以掛起,重新計算nanosTimeOut的時間,在nanosTimeOut掛起當前線程,在nanosTimeOut沒有獲取到同步狀態,直接返回false,在nanosTimeOut < spinForTimeoutThreshold(1000納秒),線程不會被掛起,而是進入快速的自旋過程,因爲非常短的時間掛起線程等待無法做到十分精確。
共享式(比如countDownLatch)
獨佔式和共享式的最大不同就是在同一時刻能否有多個線程獲取同步狀態,通過調用acquireShared方法獲取同步狀態。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
/**
* 以共享非中斷獲取同步狀態
*/
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);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
tryAcquireShared是留給子類去重寫的,如果tryAcquireShared方法返回值<0,說明獲取同步狀態失敗,執行doAcquireShared方法,在doAcquireShared再次調用tryAcquireShared方法,判斷其返回值,若返回值<0,獲取同步狀態失敗,需要進入同步隊列進行等待,若返回值 >= 0,如果返回值=0,說明當前線程獲取同步狀態成功,其他線程無法獲取,也就不需要喚醒它的後繼節點進行傳播。如果返回值>0,此時當前線程獲取同步狀態後要喚醒它的後繼節點,讓其他線程也嘗試去獲取同步狀態。
獨佔式獲取同步狀態之後,直接返回中斷狀態,結束流程,共享式則調用setHeadAndPropagate方法傳播喚醒的動作。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // 保存當前的頭節點
setHead(node); // 把當前節點設爲頭節點
/*
* 這裏有三種情況執行喚醒操作:1.propagate > 0,代表後繼節點需要被喚醒
* 2. h節點的ws < 0或者 h=null
* 3. 新的頭結點爲空 或者 新的頭結點的ws < 0
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next; // 找到當前節點的後繼節點s
if (s == null || s.isShared()) // s=null 或者 s是共享模式,調用doReleaseShared方法喚醒後繼線程
doReleaseShared();
}
}
接着看一下doReleaseShared方法,這個方法比較複雜。
private void doReleaseShared() {
/*
* 注意,這裏的頭結點已經是上面新設定的頭結點了,從這裏可以看出,如果propagate=0,
* 不會進入doReleaseShared方法裏面,那就有共享式變成了獨佔式.
*/
for (;;) { // 這裏一個死循環直到滿足條件h=head才能跳出
Node h = head;
if (h != null && h != tail) { // 前提條件-當前的頭結點不爲null && h不是尾節點
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { // 如果當前頭結點的ws=signal,利用CAS把h的ws設爲0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h); // 喚醒頭結點的後繼節點
} // 如果h的ws=0,就把h的ws設爲PROPAGATE,表示可以向後傳播喚醒
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head) // 如果頭結點沒有發生改變,表示設置完成,可以退出循環
break; // 如果頭結點發生了變化,可能被喚醒的其他節點重新設置了頭結點
} // 這樣頭結點發生了改變,要進行重試,保證可以傳播喚醒信號
}
最後看一下同步狀態的釋放
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { //
doReleaseShared();
return true;
}
return false;
}
通過tryReleaseShared方法獲取返回值,如果返回值>=0,還是調用doReleaseShared方法去釋放,
釋放成功直接返回true,釋放後的同步狀態獨佔式和共享式都能被喚醒嘗試獲取。
AQS主要實現了獨佔式和共享式,獨佔式無非就是同步狀態在0與1之間切換,同一時刻只有一個線程獲取鎖進行操作,其他線程掛起,ReentrantLock就是個經典的獨佔式鎖。共享式的PROPAGATE的數值>0,可以使同一時刻有多個線程獲取鎖,如果PROPAGATE<0,則就變成了共享模式,可以參照Semphore。