目錄
寫在前面
AQS是整個JDK併發包的核心,要想理解其它的鎖、同步器、併發容器等的實現必須先過AQS這個坎,由於Doug Lea的思維太牛x,源碼中有一些非常細小的細節確實未能參透,如有參透其中緣由的還望留言相告。
總體介紹
基於隊列的抽象同步器,它是jdk中所有顯示的線程同步工具的基礎,像ReentrantLock/DelayQueue/CountdownLatch等等,都是藉助AQS實現的。Java中已經有了synchronized關鍵字,那麼爲什麼還需要來這麼一出呢?因爲AQS能實現更多維度,更多場景的鎖機制,例如共享鎖(讀鎖)/基於條件的線程阻塞/可以藉助它實現公平和非公平的鎖策略/可以實現鎖等待的中斷,而synchronized關鍵字由JVM實現,在代碼使用層面來說,如果僅僅是使用獨佔鎖,那synchronized關鍵字比其它的鎖實現用起來方便。
下面步入正題,來看看AQS都提供了哪些能力。
主體結構
先來看看內部的主體結構:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
protected AbstractQueuedSynchronizer() { }
//內部類Node,代表每一個獲取或者釋放鎖的線程
static final class Node{}
//head 和 tail構成了同步隊列鏈表的頭節點和尾節點。
private transient volatile Node head;
private transient volatile Node tail;
//同步狀態值,所有的同步行爲都是通過state這個共享資源來實現的。
private volatile int state;
//條件對象,用於同步在當前鎖對象上的線程
public class ConditionObject implements Condition, java.io.Serializable{}
/******
一系列的內部方法:
包括嘗試獲取鎖權力
嘗試獲取鎖失敗後構造節點放入等待隊列
各種入隊出隊的操作
嘗試釋放鎖等
********/
//一系列的Unsafe操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
}
所以總結起來AQS就是通過一系列的Unsafe方法去操作一個鏈表隊列,而鏈表隊列中每個節點需要操作的共享資源就是一個int state字段,如果要用到條件等待,則需要了解ConditionObject。
節點類的構造
AbstractQueuedSynchronizer中的鏈表是通過Node節點對象來構造的,Node是其內部類,看看Node節點的內部結構:
static final class Node {
//常量,見nextWaiter屬性
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
//waitStatus 常量值。表示等待獲取鎖的線程已經被取消(線程中斷/等待超時)
static final int CANCELLED = 1;
//waitStatus 常量值。表示後繼線程需要unpark
static final int SIGNAL = -1;
//waitStatus 常量值。表示線程正在Condition隊列中
static final int CONDITION = -2;
//waitStatus 常量值。表示線程的acquireShared行爲需要無條件的向隊列的下一個節點傳遞。用在共享鎖的場景。
static final int PROPAGATE = -3;
//注意,waitStatus除了以上常量值以爲,由於是int類型,則默認是0
volatile int waitStatus;
//前繼節點
volatile Node prev;
//後繼節點
volatile Node next;
//節點所代表的線程
volatile Thread thread;
//如果節點再Condition等待隊列中,則該字段指向下一個節點,如果節點在同步隊列中,則爲一個標誌位,其值爲 SHARED或者null
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
圖形化同步隊列和condition隊列
根據代碼中的註釋我們不難想象出同步隊列的樣子:
同步隊列是一個雙向鏈表,通過節點的next和prev進行前後的連接,在同步隊列中nextWaiter用來標識節點是共享模式還是獨佔模式,尾節點的nextWaiter和next都指向空。
我們再來看看節點在Condition等待隊列中應該長如下這個樣子:
condition隊列是一個單向隊列,節點是通過nextWaiter進行連接欸但,尾節點的nextWaiter指向null。
以上是同步隊列和condition隊列的初步樣子,在真實的實現上,還會有額外的應用來指向它們
從AQS的主體結構我們可以看出,同步隊列還應該有head和tail,再結合節點加入同步隊列的邏輯,我們得出同步隊列最終的樣子:
首先看一下節點加入同步隊列的方法:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); //用當前線程和mode類型構造節點,此時waitStatus默認爲0
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter方法是將線程放入隊列的方法,開始構造new Node(Thread.currentThread(), mode)這樣的節點,然後嘗試入隊列,如果入隊列失敗則進入enq方法,在enq中,如果隊列爲空(當t==null的時候,因爲入隊操作是從隊尾進行的),此時構造了一個空的Node節點,然後將head和tail指向它。我們稱這個節點爲哨兵節點,爲什麼需要這樣一個節點,後面會說明,哨兵節點入隊列後我們通過當前線程構造的的節點new Node(Thread.currentThread(), mode)再入隊列。由於構造方法沒有傳入waitStatus值,** 所以此時waitStatus默認爲0 **。
所以綜合AQS來看,最終的同步隊列應該長如下這個樣子:
同步隊列中head指向的節點中的thread永遠爲空(這個後面邏輯可以看到),tail永遠指向最後一個節點。
那麼在Condition等待隊列中呢? Condition隊列中有firstWaiter和lastWaiter分別指向頭節點和尾節點。
先看下加入Condition隊列的代碼:
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//構造waitStatus等於CONDITION的NODE節點
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null) //如果隊列爲空,則新節點入隊列,頭節點指向新節點
firstWaiter = node;
else
t.nextWaiter = node; //如果隊列不爲空,則新節點加入到隊列末尾
lastWaiter = node; //尾節點指向新節點
return node;
}
所以,最終condition隊列如下:
condition隊列中的節點是單向的,並且沒有哨兵節點,在condition隊列中,nextWaiter指向下一個節點,而不是像在同步隊列中那樣指向模式(SHARED或者null,其中各個節點的waitStatus等於CONDITION(-2)。
鎖實現
看完了AQS的內部結構和實現,接下來我們看一下AQS是如果去操作共享變量並且利用同步隊列來實現鎖等待和釋放的。 由於不同鎖的實現對AQS的具體實現不同,所以我們這裏只拿ReentrantReadWriteLock來舉例,主要是方便切入AQS的方法。
寫鎖(獨佔鎖)
獲取鎖
ReentrantReadWriteLock中獨佔鎖獲取鎖的方法:
public void lock() {
sync.acquire(1); //調用同步器的acquire方法
}
這裏同步器由ReentrantReadWriteLock來繼承AQS後具體實現,不同的鎖對AQS的實現是不一樣的,鎖一般主要是對AQS的tryAcquire,tryAcquireShared,tryRelease,tryReleased等方法進行實現,這裏不是該文討論的重點,所以忽略, 我們直接看同步器acquire方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) && //嘗試獲取鎖失敗
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //並且節點入隊列成功
selfInterrupt(); //線程中斷
}
整個acquire方法其實就是3部曲:
- ①嘗試獲取鎖
- ②如果獲取鎖沒有成功,則構造節點加入等待隊列中
- ③如果節點入隊列成功,則線程自我中斷讓出資源。當線程進入同步隊列中後就實現了獲取鎖過程中的線程阻塞功能了,因爲這個時候線程是自我中斷狀態
剛纔說了tryAcquire這種方法是由具體的鎖來實現的,這裏我們簡單提一下以方便理解:tryAcquire主要的目的是去操作state,然後cas改變它,如果改變成功則說明獲取鎖成功,如果不成功獲取鎖失敗,鎖的重入也是在這裏實現的,還有的同步器實現並沒有改變state,而只是判斷state是否爲0(CountDownLatch),如果對這一部想詳細瞭解的請參考對應的鎖的實現。
這裏我們直接看獲取鎖失敗的情況下,接下來的邏輯:addWaiter。
節點加入同步隊列
//在寫鎖的情況下,這裏mode爲Node.EXCLUSIVE,其實就是null
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//在enq之前的這段代碼是一個快讀入隊列的實現,如果入隊不成功則交由enq來實現
Node pred = tail; //緩存tail節點,因爲從隊尾加入節點,所以理論上tail就是前繼節點
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) { //當隊尾不等於空,則用CAS方法修改tail節點,讓其指向本次的構造的新節點。
pred.next = node; //如果CAS成功則說明入隊成功,然後將前繼節點的next指向新節點。
return node;
}
}
//如果隊列爲空,或者新節點入隊列失敗,則交給enq處理。
enq(node);
return node;
}
從上面的代碼我們能看出AQS中一個很重要的邏輯:入隊列,一定是優先保證tail的重新指向,然後纔是前節點的next指向新節點。如果拿分佈式系統數據一致性舉例的話就是:tail是數據強一致性,而next是最終一致,這一點在後面的邏輯判斷中很重要。所以判斷tail是否爲空就能判斷隊列是否爲空。
在簡單的嘗試入隊列失敗後,代碼會進入到enq方法,用自旋的方式保證節點一定能入隊列:
//自旋的入隊列方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize //隊列爲空
if (compareAndSetHead(new Node())) //構造空節點,CAS改變頭節點
tail = head; //將tail指向新節點
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { //隊列不爲空,則將tail指向新節點
t.next = node; //前節點的next指向新節點
return t;
}
}
}
}
這裏隊列不爲空的情況與addWaiter的快速入隊邏輯是一樣的沒什麼好說的,這裏的重點是隊列等於空的時候,我們可以看到代碼構造了一個不包含任何線程的“空”節點,然後將head指向它,這裏有兩個問題: 1.爲什麼要構造這個空節點,直接用新節點作爲頭節點不好麼? 2.爲什麼隊列爲空的時候是先修改head而不是像隊列不爲空的時候那樣直接修改tail呢。
這裏先保留這兩個問題,後續再來解答。
這一節主要記住的一個核心點就是:隊列是否爲空通過tail判斷,一個節點加入隊列分爲兩步,第一步是強一致性的修改tail(節點的pre已經指向的前節點),下一步再是修改前節點的next。
節點加入同步隊列成功後,接下來就是重新嘗試獲取鎖:
同步隊列中的節點嘗試獲取鎖或者睡眠
線程被構造爲節點進入隊列後,接下來就是對隊列中的節點進行獲取鎖的處理:
//獨佔模式並且不可中斷的爲同步隊列中的線程獲取鎖
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); //如果獲取鎖成功,則將頭節點指向獲取鎖成功的節點,並清空該節點的thread和pre,讓該節點變成新的哨兵
p.next = null; // help GC
failed = false;
return interrupted; //返回中斷狀態
}
//如果獲取鎖失敗,或者當前節點不是最靠前的非哨兵節點,則嘗試將線程park
if (shouldParkAfterFailedAcquire(p, node) && //判斷當前節點是否可以park,如果可以則嘗試park
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); //如果獲取鎖失敗,取消競爭鎖
}
}
//判斷當前節點是否可以睡眠(park)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //如果前繼節點的waitStatus等於Node.SIGNAL,則前節點會負責喚醒當前節點, 當前節點可以park
return true;
if (ws > 0) { //如果前節點已經取消(被中斷或超時)
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0); //直到遍歷到非取消的前節點爲止
pred.next = node;//將遍歷到的前節點next指向node節點
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//將前節點waitStatus更新爲Node.SIGNAL,表示後繼節點需要unpark
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//park當前線程,並返回當前線程的中斷狀態
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
看到這裏,我們來說一下剛纔遺留的那個問題:爲什麼要增加一個哨兵節點。如果沒有哨兵,如上圖,這個時候該節點獲取到鎖,這個時候需要做兩件事情,先判斷head是否等於tail,然後需要將head指向當前的next,其實就是指向null,tail也需要指向null,這個時候,如果有另外一個線程正在入隊列,需要將head或者tail指向這個入隊列新節點,這個時候如果入隊節點剛完成tail以及pre的指向,還沒來及更改前節點的next,這時出隊列這邊則會將head指向null,tail也會被指向null,當然這裏如果在將tail指向null之前進行一次當前tail與老tail的對比, 如果一致則更新爲null,不一致則不更新能避免tail指向null,但是head這邊就顯得比較麻煩了,每次獲取鎖成功要重新設置head都需要從隊尾向對頭遍歷,以防止有新的節點放入,直到遍歷到節點中thead爲空的(獲取鎖已出隊列)節點的後一個節點爲止。 這樣的處理顯得相當的麻煩。
第二個問題是爲什麼要先修改head而不能直接修改tail,先看有哨兵的情況,我改寫了一下enq的方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize //隊列爲空
Node n = new Node();
if (compareAndSetTail(n)) //構造空節點,CAS改變頭節點
head = n;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { //隊列不爲空,則將tail指向新節點
t.next = node; //前節點的next指向新節點
return t;
}
}
}
}
這樣的處理方式我認爲它是線程安全的。在沒有哨兵的情況下就是把n換成node,邏輯是一樣的。
獲取鎖或者睡眠過程中線程被中斷(取消獲取鎖)
我們再來看看取消獲取鎖做了什麼事情:
//取消獲取鎖
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null) //空判斷
return;
node.thread = null; //取消節點對線程的引用
//將node節點往前繼節點方向所有連續的取消狀態的節點出隊列
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED; //修改node狀態
// If we are the tail, remove ourselves.
//如果當前節點是隊尾,則將當前節點移除隊列,這個時候就不用設置前節點狀態
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null); //將前節點(這個時候已經是隊尾節點)的next指向null,這裏不保證它一定會成功,因爲可能有其它新節點加入,用CAS方式避免將覆蓋其它線程的操作。
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head && //如果當前節點不是隊尾,也不是最靠前的節點
((ws = pred.waitStatus) == Node.SIGNAL || //前節點狀態已經爲Node.SIGNAL 或者 將前節點狀態更改爲Node.SIGNAL成功
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) { //並且前節點還未出隊列
Node next = node.next;
if (next != null && next.waitStatus <= 0) //當前節點的next節點存在並且沒有取消
compareAndSetNext(pred, predNext, next);//則將前節點的next指向node節點的next
} else {
unparkSuccessor(node); //喚醒後繼節點 //node爲頭節點,或者更新前節點狀態爲SIGNAL失敗(前節點就沒辦法自動喚醒後節點了),或者前節點thead==null(前節點已取消)
}
node.next = node; // help GC 後繼節點指向自己,去除引用,幫助GC
}
}
其主要邏輯如下圖:
這種情況下,尾節點直接出隊列,tail指向pred節點。
這兩個圖相同的邏輯就在於將當前節點狀態修改爲取消,然後thead置空,最後出隊列,但是這裏沒有重新設置當前節點的prev指針,如果從tail向前遍歷,還是能遍歷到node節點。
取消節點需要喚醒後繼節點的情況
我們再來看下,當前節點狀態更新爲SIGNAL失敗(這是哪種情況?),或者當前節點就是首節點時,喚醒後繼節點是如何操作的:
//喚醒node節點的後繼節點
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
//在喚醒後繼之前,先將node節點狀態改爲0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//如果node的後繼節點被取消,則從隊尾向node節點遍歷,找到距離node節點最近的waitStatus<=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);
}
這裏有一個問題就是取消當前節點獲取鎖,然後要喚醒後繼節點的時機,爲什麼需要喚醒後繼節點?
我們仔細看下進入喚醒後繼節點的else代碼:
if (pred != head && //如果當前節點不是隊尾,也不是最靠前的節點
((ws = pred.waitStatus) == Node.SIGNAL || //前節點狀態已經爲Node.SIGNAL 或者 將前節點狀態更改爲Node.SIGNAL成功
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//do something
} else {
unparkSuccessor(node);
}
其中if判斷爲false的情況有:
1.pred == head : 前節點爲head節點,說明當前節點爲第一個有效節點,如果當前節點被中斷了,head節點在喚醒後繼節點時可能會找到老節點(當前移除的節點),所以需要手動喚醒node的後繼節點
2.pred != head的情況,pred.waitStatus != Node.SIGNAL 並且ws<=0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 爲false :即node的前繼節點非head的情況,前繼節點狀態不等於SIGNAL (ws>0即等於CANDLED,或者ws=[0, CONDITION, PROPAGATE])但是更新狀態爲SIGNAL失敗,這裏ws>0是讓後繼節點醒來,在acquireQueued方法中重新判斷是否獲取鎖還是睡眠。其它情況將狀態更新爲SIGNAL失敗,則說明前節點剛好完成鎖釋放並執行了unparkSuccessor,在該方法中,需要將當前釋放鎖節點的狀態更新爲0
3.當pred!=head,前繼節點狀態也等於SIGNAL的時候,pred.thread == null, 這種情況唯有前繼節點爲取消狀態線程纔會爲空,如果前繼節點已經取消,則應該喚醒後繼節點,重新處理acquireQueued。
釋放鎖
這裏還是從ReentrantReadWriteLock可重入讀寫鎖入手,查看它的釋放寫鎖的實現:
public void unlock() {
sync.release(1);
}
可以看到,通過同步器調用release方法來達到釋放鎖的目的。
重點看一下AQS中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由具體的鎖來實現。
共享鎖
共享鎖特性:
1.持有獨佔鎖的線程能夠再獲取共享鎖,但是獲取共享鎖的線程不能再獲取獨佔鎖。
2.獲取共享鎖的線程能夠再獲取共享鎖(重入),不同線程能夠獲取同一把共享鎖。
獲取共享鎖
還是以ReentrantReadWriteLock鎖的實現爲例,首先看下獲取讀鎖的代碼入口:
public void lock() {
sync.acquireShared(1);
}
很明顯也是調用同步器來實現的,這裏主要關注acquireShared。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
整體邏輯與獲取寫鎖類似,首先tryAcquireShared,該方法的具體實現邏輯有鎖實現來決定,在tryAcquireShared中,鎖的實現會根據state來判斷,如果當前已經加了寫鎖,則不能加讀鎖,如果當前有其它線程獲取的讀鎖,則本次同樣能加讀鎖,並且會判斷是否同一個線程,如果是,則重入次數加1。 如果獲取鎖不成功,則進入doAcquireShared方法。
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); //如果前節點已經是head節點,則繼續嘗試tryAcquireShared
if (r >= 0) { //獲取讀鎖成功
setHeadAndPropagate(node, r); //獲取鎖成功,並且喚醒後續需要獲取共享鎖的節點
p.next = null; // help GC
if (interrupted) //線程unpark後,會判斷線程的中斷狀態,如果線程已經被中斷,這裏繼承中斷狀態
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); //獲取共享鎖過程中的任何異常都需要取消該節點繼續獲取鎖,詳情建寫鎖的取消邏輯。
}
}
共享鎖的核心邏輯在於setHeadAndPropagate方法,該方法中實現了共享鎖獲取鎖的冒泡。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node); //把當前節點變成頭節點, 並且將thread引用置空,prev置空
if (propagate > 0 ||
h == null ||
h.waitStatus < 0 ||
(h = head) == null ||
h.waitStatus < 0) { //喚醒後繼節點中所有的共享模式的等待者
Node s = node.next;
if (s == null || s.isShared()) //後繼節點有可能正在加入隊列(在addWaiter中是cas保證tail成功,然後再設置的next引用),或者後繼節點是共享模式,都需要嘗試喚醒後繼節點。
doReleaseShared(); //這裏不是釋放鎖, 是喚醒後繼節點。
//對於共享模式而言,前者獲取到共享鎖後,需要喚醒後繼的共享鎖等待者;這與前繼節點釋放鎖後需要喚醒後繼節點邏輯一致,所以作者把通通的共享模式下喚醒後繼節點的行爲封裝爲了一個方法。
}
}
從上面代碼可以看出,判斷是否需要繼續往後傳遞獲取鎖的行爲取決於緊鄰的後繼節點的模式是否爲Shared。如果爲Shared模式,則需要向後傳遞獲取鎖的行爲,具體邏輯看doReleaseShared的實現。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) { // 有至少一個後繼節點
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { //後繼節點需要喚醒
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //這裏爲什麼一定需要CAS成功才能喚醒後繼節點,因爲在共享模式下,後繼節點可能被喚醒後很快釋放鎖,如果前節點狀態還是爲SIGNAL,則等它釋放鎖時需要去喚醒後繼節點,此時它的後繼已經釋放鎖了,這裏就會有問題,其實就是併發的問題。
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //如果節點狀態已經爲0,則說明要麼節點已經執行了unparkSuccessor,或者沒有後繼節點了。
continue; // loop on failed CAS
}
//如果在執行以上動作中,有新節點獲取到鎖(誰獲取到鎖,head指向誰),從新嘗試喚醒新節點的後繼節點。
if (h == head) // loop if head changed
break;
}
}
doReleaseShared方法是一個自旋方法,首先判斷是否還有後繼節點if (h != null && h != tail),如果有後繼節點,拿到當前頭節點狀態,如果頭節點狀態爲SIGNAL,則需要喚醒後繼節點,這裏後繼節點可能是共享節點,也可能是一個獨佔節點(才掛上來的),我們注意到代碼在執行喚醒後繼節點unparkSuccessor之前,先執行了if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)),這裏的邏輯顯然於獨佔模式釋放鎖喚醒後繼節點或者共享模式釋放鎖的邏輯不太一樣,這裏是強制優先講頭節點狀態設置0,只有設置爲0成功的線程才能執行喚醒下一個節點的操作。這裏之所以要這樣設計,是因爲共享模式下,一次釋放鎖的線程可能有多個,甚至存在有的共享節點在釋放鎖,有的共享節點卻在獲取鎖,但是不管怎麼樣,他們都會併發的調用doReleaseShared方法,如果這裏不加以控制,則會重複的調用unparkSuccessor,最後會重複的調用unpark方法(這裏我還沒明白如果重複調用unpark會有什麼問題)。
那麼剛纔沒有compareAndSetWaitStatus(h, Node.SIGNAL, 0)成功的節點會再次循環,再次進來時發現此時頭節點狀態爲0(因爲其它線程執行了喚醒操作),則講狀態更新爲PROPAGATE,這裏重點說下這個邏輯,我翻遍了所有的代碼都沒有發現哪裏有指定將PROPAGATE變更爲其它狀態的邏輯,所以我不指定作者這裏設計一個這個狀態有何用,我認爲更不需要compareAndSetWaitStatus(h, 0, Node.PROPAGATE)這段邏輯,當其線程再次進入判斷髮現頭節點狀態不爲SIGNAL時,直接結束。這種情況下再來看剛纔的喚醒操作,當喚醒後的節點重新嘗試獲取鎖失敗時,它又會執行shouldParkAfterFailedAcquire方法,然後將前節點更改爲SIGNAL,如果喚醒後的節點獲取鎖成功,則head節點指向該節點,後續的喚醒操作只與該節點有關。所以真心不明白爲什麼要有段設置爲PROPAGATE的邏輯,主要是代碼中任何地方看不到使用了PROPAGATE的這個邏輯。
這裏再說下自旋,退出自旋的條件是if (h == head),即在處理過程中沒有新的節點獲取到鎖,用反證法,假設有新的節點獲取到鎖,這種情況下如果退出了方法會有問題麼 ? 我認爲沒有問題,因爲獲取到鎖的節點最終會釋放鎖,釋放鎖的動作又會喚起後繼節點,所以爲什麼要自旋呢?
以上確實有一些細節沒有琢磨透
這裏我們可以來看下整個node節點的狀態變化:
node節點狀體變化圖
通過上圖觀察,所有從0變到PROPAGATE的,最後都會因爲新加入節點或者節點取消而變稱SIGNAL,而SIGNAL最終又都會因爲需要喚起後繼節點而將當前節點更新爲0,這形成了一個完整的閉環,那爲什麼不是直接0變爲SIGNAL,不知道PROPAGATE在中間起到什麼作用。
釋放鎖
相較於獲取讀鎖,釋放的過程就比較簡單了
public void unlock() {
sync.releaseShared(1); //調用同步器的releaseShared
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
可以看到, 在嘗試獲取鎖成功後,直接調用doReleaseShared喚醒後繼節點。這裏的邏輯就與共享鎖獲取鎖後需要繼續喚醒緊鄰的後繼共享節點一樣。
還有從這裏就可以看出,doReleaseShared方法是被多個線程同時調用的,所以在代碼裏面unparkSuccessor處需要進行併發的控制。
帶中斷的獲取鎖
不管是讀鎖還是寫鎖,在獲取鎖的過程中還有一類獲取方法是在等待鎖的過程中允許線程被中斷的,方法會拋出InterruptedException異常。
這裏用共享鎖的lockInterruptibly方法舉例:
public void lockInterruptibly() 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);
}
接下來看下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與doAcquireShared方法相比,唯一的區別就在於線程被unpark後如何處理中斷的,在doAcquireShared中,是將中斷狀態設置爲一個boolean標誌返回給調用者,讓調用者自行決定該如何處理,而在doAcquireSharedInterruptibly中,如果發現線程被中斷,直接拋出中斷異常終止程序。
公平/非公平同步器
公平與非公平同步器其實與AQS本身無關,這都是AQS具體的實現,不同的鎖有不同的實現方式,但是總體的規則是一致的。因爲是AQS的實現,我想有必要這裏說一下,具體的實現請參考各個鎖的實現。
公平同步器:
所謂公平,即遵循先到先得的原則,誰先到達隊列,則誰就優先獲取鎖,不得在剛嘗試鎖的時候不管隊列中是否有等待中的節點直接競爭鎖。
而所謂不公平其實就是當一個線程嘗試獲取鎖的時候,不會主動的去判斷同步隊列中是否有等待的節點,直接就去競爭鎖,如果競爭失敗則進入同步隊列進行等待。
ConditionObject條件等待
在AQS中還有一個比較重要的類:ConditionObject,這個類實現了在基於AQS鎖的情況下對獲取到鎖的線程進行有條件的等待和喚醒,其主要的方法是await和signal以及它們的變種。有很多博客都拿它與Object對象的wait和notify作比較,說ConditionObject的await和signal運用的地方要比wait和notify廣,其實並不然。它們使用的場景是截然不同的,不然的話Doug Lea也不會費這力氣重新造一個輪子。
我認爲他們的區別主要在於,Object的wait和notify其實是在任何情況下都是可以調用的,而ConditionObject的await和signal必須要在基於AQS的鎖環境下才能調用,不然就會拋出異常(這也是我認爲它們之間最大的差異);其次,ConditionObject是專門爲AQS服務的,它的節點的構造,狀態的標誌等都與AQS有關,在wait操作和notify操作時都需要去操作AQS的同步隊列。
所以綜上所述,ConditionObject是專門爲AQS服務的,而不像有的博客寫的它的用途要比Object的實現要廣。
接下來看看ConditionObject的具體實現。
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
/**
* Creates a new {@code ConditionObject} instance.
*/
public ConditionObject() { }
/***
methods
***/
}
它的核心結構就是有一個Node的頭節點和尾節點,然後Node中通過Node nextWaiter進行關聯形成了一個Condition鏈表隊列,後續所有的操作都是圍繞這個隊列和同步隊列來進行的。
await方法
public final void await() throws InterruptedException {
if (Thread.interrupted()) //判斷當前線程是否被中斷
throw new InterruptedException();
Node node = addConditionWaiter(); //①將當前線程構造爲Condition等待節點並加入隊列,詳情見addConditionWaiter說明
int savedState = fullyRelease(node); //②釋放鎖現在全部的狀態,鎖可能有重入,所以這裏不是直接調用AQS的release方法,詳情見fullyRelease說明
int interruptMode = 0; //標記線程在await過程中的中斷狀態,0表示未中斷
while (!isOnSyncQueue(node)) { //判斷node節點是否在同步隊列中,只有node節點進入了同步隊列循環纔會結束(即,被signal了)
LockSupport.park(this);//如果不在同步隊列中, 則park當前線程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //③線程被喚醒或者中斷後,判斷線程的中斷狀態
break; //如果線程被中斷過,則退出循環
}
//④線程被喚醒後重新獲取鎖,鎖狀態恢復到savedState
//不管線程是:未中斷,還是signal中斷,singnal後中斷,前面的代碼 都會保證node節點進入同步隊列。
//acquireQueued 方法獲取到鎖,並且在獲取鎖park的過程中有被中斷,並且之前在await過程中,不是被signal之前就中斷的情況,則標記後續處理中斷的情況爲interruptMode。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//⑤重新獲取到鎖,把節點從condition隊列中去除,同時也會清除被取消的節點
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//⑥線程被中斷,根據中斷條件選擇拋出異常或者重新中斷傳遞狀態
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
//向Condition隊列添加等待者
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) { //如果隊尾節點已經取消,則先清空隊列中的取消節點
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); //加入到Condition隊列中的節點狀態都爲CONDITION
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
從以上代碼可以看出:condition隊列中,節點是通過nextWaiter來形成鏈表的,隊列中所有節點的狀態爲CONDITION。
//釋放節點
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;
}
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
從fullyRelease的實現可以看出:release方法調用了tryRelease,如果tryRelease成功,則喚醒後繼節點,這裏要注意的是如果release不成功,則會拋出IllegalMonitorStateException異常,其實我認爲這裏的else裏面拋出異常是多餘的,因爲就tryRelease的實現來看,如果不是本線程去釋放自己獲得的鎖,tryRelease本身就會拋出IllegalMonitorStateException異常的,而如果是本線程在釋放鎖,那一定是在持有鎖的情況下來釋放鎖的,這種情況一定會成功的,所以根本不會release失敗,所以代碼怎麼都進不到else中去。但是可以總結出的是,await等方法一定是要在線程獲取AQS鎖的情況下調用,否則就會拋出異常。另外,如果在釋放過程中線程中斷,則將節點設置爲CANCELLED。
③檢查中斷
//檢查Condition隊列中節點在等待過程中的中斷狀態
//THROW_IE:表示在signal之前被中斷喚醒
// REINTERRUPT:表示在signal之後有中斷,在singnal之後被通斷,需要保證singnal的行爲最終完成,所以中斷只用延續狀態狀態REINTERRUPT,不用拋出異常。
/**
* Checks for interrupt, returning THROW_IE if interrupted
* before signalled, REINTERRUPT if after signalled, or
* 0 if not interrupted.
*/
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ? //當前線程中斷狀態
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
final boolean transferAfterCancelledWait(Node node) {
//如果節點是被signal喚醒,則狀態會被更新爲0,然後入同步隊列,最後纔是被unpark,所以這裏如果能CAS成功,則說明節點沒有被signal,所以線程是在await過程中被中斷的。所以在這裏需要將節點入隊列。
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node); //節點入同步隊列
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
//如果節點是被signal喚醒的,則節點應該會在同步隊列中,什麼情況下被signal喚醒但是node節點不在同步隊列中,而等待一會兒就在同步隊列中了,這點確實沒想明白。
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
signal喚醒等待線程
signal的用途是喚醒調用await方法後進入park的線程。主要代碼如下:
public final void signal() {
if (!isHeldExclusively()) //判斷當前線程是否是鎖的持有者,如果不是則拋出異常
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null) //頭節點不爲空則鏈表不爲空
doSignal(first);
}
下面詳細看下doSignal的實現:
//喚醒在該條件上等待時間最長的且狀態正常的節點
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && //循環從first嘗試將節點轉換爲同步隊列節點,直到轉換成功或者遍歷完鏈表。
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
//如果CAS失敗,則說明節點狀態不爲CCONDITION,則返回false繼續嘗試下一個節點。
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node); //將node節點加入同步隊列
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) //如果隊列中前節點被取消或者CAS前節點狀態爲SIGNAL失敗,手動喚醒已經進入同步隊列的node節點
LockSupport.unpark(node.thread);
return true;
}
從代碼邏輯結合ReentrantReadWriteLock的實現:
1.aqs的Condition需要在獲取鎖的情況下使用
2.共享鎖不支持Condition
總結
AQS的邏輯相當複雜,但是我們不難看出其核心就是讓獲取鎖的線程進入隊列等待,然後釋放鎖的節點負責喚醒後繼節點。