AbstractQueuedSynchronizer源碼解析
AbstractQueuedSynchronizer是同步器,簡稱AQS,是各類線程鎖的基礎。該類的方法很多,針對包裝線程的節點的操作也很多。
1.整體架構
AQS整體處理流程如下,
對上圖進行說明,
- AQS中的隊列有兩種,同步隊列和條件隊列,其底層數據結構是鏈表
- 四種顏色的線條代表不同的場景
AQS本身就是一套鎖的框架,定義了獲得鎖和釋放鎖的代碼,所以繼承AQS抽象類並實現相應的方法即可實現鎖。
類註釋
AQS的類註釋中包含的信息如下,
- AQS提供了一個框架,定義了先進先出的同步隊列,讓獲取不到鎖的線程在同步隊列中排序
- 同步器存在一個成員變量
status
,表示同步器的狀態,用於判斷AQS是否能夠得到鎖。該變量使用volatile
關鍵字進行修飾保證其線程安全 - AQS的子類可以通過CAS的方式給
status
賦值,定義哪些狀態可以獲取鎖,哪些狀態獲取不到鎖 - AQS提供兩種鎖模式,共享鎖和排它鎖。
i) 排他模式:只有一個線程可以獲得鎖
ii)共享模式:多個線程可以同時獲得鎖
AQS的子類 ReadWriteLock實現了這兩種模式 - AQS的內部類ConditionObject是條件隊列的實現類。通過
new Condition()
可以創建一個條件隊列。鎖對象中,條件隊列的數目可以是多個 - AQS繼承了 AbstractOwnableSynchronizer類,該類可以追蹤獲得鎖的線程
類定義
AQS的類定義如下,
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {}
- AQS是抽象類,該類中只實現類將線程放入和取出同步隊列、條件隊列的方法,定義瞭如何獲得鎖、如何釋放鎖的抽象方法,目的是讓子類去實現爭鎖和釋放鎖的過程
- 繼承了AbstractOwnableSynchronizer類,該類的作用就是爲了知道哪個線程獲取了鎖,便於管理
類屬性
1)一般屬性
// 同步器狀態,子類會根據狀態字段進行判斷是否可以獲得鎖或釋放鎖
// CAS給該變量賦值,獲得鎖+1,釋放鎖-1
private volatile int state;
// 自旋超時閥值,單位納秒。當設置等待時間時纔會用到這個屬性
static final long spinForTimeoutThreshold = 1000L;
2)同步隊列屬性
同步隊列的說明如下,
- 當多個線程都來請求鎖時,某一時刻有且只有一個線程能夠獲得鎖(排它鎖)。其餘獲取不到鎖的線程,都會到同步隊列中去排隊並阻塞自己。
- 當有線程主動釋放鎖時,就會從同步隊列頭開始釋放一個排隊的線程,讓線程重新去競爭鎖。
- 同步隊列的底層是雙向鏈表
private transient volatile Node head;
private transient volatile Node tail;
同步隊列的頭和尾,是AQS的屬性。
3) 條件隊列屬性
條件隊列的說明如下,
- 條件隊列同樣用於管理獲取不到鎖的線程
- 條件隊列的底層是單向鏈表(與同步隊列區分)
- 條件隊列不直接和鎖打交道
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
// 條件隊列中第一個 node
private transient Node firstWaiter;
// 條件隊列中最後一個 node
private transient Node lastWaiter;
}
條件隊列實現的是 Condition接口。
4)Node
Node既是同步隊列的節點,同時也是條件隊列的節點。該類用於包裝線程,
static final class Node {
// 同步隊列單獨的屬性
//node共享模式或排他模式
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
// 當前節點的前節點
// 節點 acquire 成功後就會變成head
// head 節點不能被 cancelled
volatile Node prev;
// 當前節點的下一個節點
volatile Node next;
// 兩個隊列共享的屬性
// 表示當前節點的狀態,通過節點的狀態來控制節點的行爲
// 普通同步節點,就是 0 ,條件節點是 CONDITION -2
volatile int waitStatus;
// waitStatus 的狀態有以下幾種
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 當前節點包裝的線程
volatile Thread thread;
// 在同步隊列中,nextWaiter並不真的是指向其下一個節點,只是表示當前 Node 是排它模式還是共享模式
// 但在條件隊列中,nextWaiter 就是表示下一個節點元素
Node nextWaiter;
}
Node類中的waitStatus
一定與AQS的status
進行區分,Node的操作是通過waitStatus
決定的。
對Node節點的狀態進行如下說明,
狀態 | 說明 |
---|---|
CANCELLED | 被取消 |
SIGNAL | 同步隊列中的節點在自旋獲取鎖的時候, 如果前一個節點的狀態是 SIGNAL,那麼自己就可以阻塞休息了;否則自己一直自旋嘗試獲得鎖 |
CONDITION | 表示當前 node 正在條件隊列中。當節點從同步隊列轉移到條件隊列時,狀態就會被更改成 CONDITION |
PROPAGATE | 無條件傳播,共享鎖模式下,該狀態的進程處於可運行狀態 |
5)共享鎖和排它鎖的區別
同一時刻只能有一個線程可以獲得排它鎖,也只能有一個線程可以釋放鎖。
共享鎖同一時刻允許多個線程獲得同一個鎖,並可以設置獲取鎖的線程的數目。
Condition接口
條件隊列ConditionObject實現了Condition接口,
1)類註釋
- 當使用鎖對象替代 synchronize時,Condition用於替代 Object中的監控方法,如
Object#wait()
、Object#notify()
和Object#notifyAll()
等方法 - 線程被暫停執行後,等待其他線程將其喚醒
- Condition實例綁定在鎖對象上,通過
Lock#newCondition()
方法可以創建鎖的條件隊列
2)條件隊列示例
假設存在一個有邊界的隊列,支持 put和 take方法存入或取出元素,
- 如果試圖向空隊列執行
take
操作,線程將會阻塞,直到隊列中有可用的元素爲止 - 如果試圖向滿隊列執行
put
操作,線程將會阻塞,直到隊列中有空閒的位置爲止
上面的例子中,如果兩個操作依靠一個條件隊列,那麼每次只能執行其中一個操作。所以可以創建兩個條件隊列,分別執行存入和取出的操作。
3)接口方法
Condition接口定義了一些方法,這些方法奠定了條件隊列的基礎,
void await() throws InterruptedException;
該方法使當前線程一直等待,直到被signal
或signalAll
方法喚醒。
條件隊列中的線程被喚醒的四種情況,
- 有線程使用了
signal
方法,喚醒了條件隊列中的當前線程。該方法喚醒條件隊列中的一個線程,在被喚醒前必須先獲得鎖 - 有線程使用了
signalAll
方法,該方法喚醒條件隊列中的所有線程 - 其他線程打斷了當前線程
- 虛假喚醒
線程從條件隊列中甦醒時,必須重新獲得鎖,才能真正被喚醒。
2.同步器狀態
AQS中存在兩個狀態,status
和waitStatus
,二者一定要區分,
- status是鎖的狀態,是 int 類型。子類繼承 AQS 時,都是要根據 state 字段來判斷有無得到鎖,比如當前同步器狀態是 0,表示可以獲得鎖,當前同步器狀態是 1,表示鎖已經被其他線程持有,當前線程無法獲得鎖;
- waitStatus 是節點(Node)的狀態,種類很多,一共有初始化 (0)、CANCELLED (1)、SIGNAL (-1)、CONDITION (-2)、PROPAGATE (-3),各個狀態的含義可以見上文。
3.獲取鎖
在AQS的子類中通常使用Lock#lock()
方法獲得鎖,使得線程能夠取得資源的使用權限。Lock是AQS的子類,lock
方法或根據情況調用AQS的acquire
和tryAcquire
方法。
acquire
方法 AQS 已經實現了,tryAcquire
方法是等待子類去實現。
- acquire 方法制定了獲取鎖的框架,先嚐試使用 tryAcquire 方法獲取鎖,獲取不到時,再入同步隊列中等待鎖
- tryAcquire 方法在 AQS 中直接拋出一個異常,表明需要子類去實現,子類可以根據同步器的 state 狀態來決定是否能夠獲得鎖
獲得排它鎖—acquire
public final void acquire(int arg) {
// tryAcquire 方法是需要實現類去實現的,實現思路一般都是 cas 給 state 賦值來決定是否能獲得鎖
if (!tryAcquire(arg) &&
// addWaiter 入參代表是排他模式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
該流程對應架構圖的紅線部分,
具體流程爲,
- 嘗試執行一次
tryAcquire
方法,如果成功則線程執行任務即可。如果失敗則走下面的流程進入等待隊列 - 調用
addWaiter
方法將線程包裝成Node,添加到同步隊列尾部。addWaiter
方法返回包裝了當前線程的Node對象 - 調用
acquireQueued
方法,該方法的作用有兩個,
i. 阻塞當前線程
ii. 節點被喚醒時,使其能夠獲得鎖
這兩個功能的實現是依靠acquireQueued
方法中自旋過程 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
如果返回值爲true
說明線程是在同步隊列中阻塞一段時間後才被取出,此時線程的interrupt
狀態爲true,調用selfInterrupt()
將interrupt
改爲false
1)addWaiter方法
用於將未獲取到鎖的線程添加到同步隊列尾部,
private Node addWaiter(Node mode) {
// 初始化包裝當前線程的Node對象
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;
}
}
// 如果隊列爲空或簡單嘗試失敗則自旋保證node加入到隊尾
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果隊尾爲空,說明當前同步隊列都沒有初始化,進行初始化
// tail = head = new Node();
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else { // 隊尾不爲空,將當前節點追加到隊尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在 addWaiter 方法中,並沒有進入方法後立馬就自旋,而是先嚐試一次追加到隊尾,如果失敗才自旋,因爲大部分操作可能一次就會成功,這種思路在寫自旋的時候可以借鑑。
2)acquireQueued方法
將線程加入到同步隊列尾部後,需要使當前線程阻塞,acquireQueued
方法用於實現這一功能,
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
final Node p = node.predecessor(); // 獲取當前線程節點的前一個節點
// 有兩種情況會走到 p == head,見下方說明
if (p == head && tryAcquire(arg)) {
// 成功獲得鎖,則將自身設置成head節點並回收其前置節點
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire和parkAndCheckInterrupt 用於阻塞當前線程
// 線程是在這個方法裏面阻塞的,醒來的時候仍然在for循環裏面,就能再次自旋嘗試獲得鎖
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果獲得node的鎖失敗,將 node 從隊列中移除
if (failed)
cancelAcquire(node);
}
}
方法的自旋過程在每一次循環開始時都會有一步判斷p == head
,滿足這一條件通常有兩種情況,
- node 之前沒有獲得鎖,
addWaiter
方法中調用enq
方法初始化同步隊列後將其添加到了新創建的head
節點之後。
此時進入 acquireQueued 方法時,才發現當前節點的前置節點就是頭節點,於是嘗試獲得一次鎖 - node節點之前一直在阻塞沉睡,然後被喚醒(喚醒操作從
head
節點開始)。此時喚醒 node 的節點正是其前一個節點。
如果 tryAcquire 成功,就立馬把自己設置成 head,把其前置節點移除(因爲前置節點是之前的head
,同步隊列的頭節點相當於一個沒有用的空節點)。
如果 tryAcquire 失敗,嘗試進入同步隊列。
對acquireQueued
方法的返回值進行說明,
- 返回
true
,說明節點是進入到同步隊列之後阻塞了一段時間才被從隊列中取出 - 返回
false
,說明節點進入了同步隊列,但是沒等到被阻塞就被從隊列中取出,即沒有執行到shouldParkAfterFailedAcquire
和parkAndCheckInterrupt
方法
setHead方法
將節點設置爲同步隊列的頭節點,
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
同步隊列的頭節點相當於一個空節點,其作用只是指向其下一個節點。
shouldParkAfterFailedAcquire方法
該方法用於將當前的Node對象的前置節點waitStatus
設置爲SIGNAL
,前置節點的狀態爲SIGNAL時,當前節點就可以阻塞。
// 參數是當前節點的前置節點和當前節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前置節點狀態已經是SIGNALED,說明當前節點不是新入隊的節點
// 該線程可以安全的被park阻塞
if (ws == Node.SIGNAL)
return true;
// 前置節點是CANCELLED狀態說明該節點無效,將當前節點掛到更前面的有效節點之後
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前置節點狀態是0或PROPAGATE說明當前節點是新入隊的節點
// 需要將前置節點的狀態改爲SIGNAL,但是不立刻對其執行park操作。而是返回false,回到自旋過程中在新一輪循環中才阻塞
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
對shouldParkAfterFailedAcquire
方法返回值的說明,
- 返回 true,前置節點狀態已經是SIGNALED,當前節點的線程可以被安全阻塞
- 返回 false,前置節點狀態剛剛被調整爲SIGNALED,當前節點的線程不立刻阻塞。而是回到
acquireQueued
方法的自旋過程中在新一輪循環中阻塞
parkAndCheckInterrupt方法
該方法用於阻塞線程,線程是在這個方法裏面阻塞的,醒來的時候仍然在acquireQueued
方法的 for 循環裏面,就能再次自旋嘗試獲得鎖,
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
獲取共享鎖—acquireShared方法
acquireShared 整體流程和 acquire 相同,代碼也很相似,貼出來不一樣的代碼進行比較,
1)嘗試獲取鎖—tryAcquireShared方法
2)setHeadAndPropagate方法
第二步不同,在於節點獲得排它鎖時,僅僅把自己設置爲同步隊列的頭節點即可(setHead
方法)。但如果是共享鎖的話,還會去喚醒自己的後續節點,一起來獲得該鎖(setHeadAndPropagate
方法)。
不同之處如下(左邊排它鎖,右邊共享鎖),
setHeadAndPropagate方法源碼如下,
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
// propagate > 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();
}
}
// 釋放後置共享節點
private void doReleaseShared() {
for (;;) {
Node h = head;
// 還沒有到隊尾,此時隊列中至少有兩個節點
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果隊列狀態是 SIGNAL ,說明後續節點都需要喚醒
if (ws == Node.SIGNAL) {
// CAS 保證只有一個節點可以運行喚醒的操作
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 進行喚醒操作
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
總結
本節的重點是獲取排它鎖的acquire
方法,整體的流程圖如下,