今天是4月4日,清明節第一天,互聯網一片灰白,大家都在緬懷逝者,致敬英烈。所以今天我也沒有過多的娛樂,一天都在鼓搗這篇文章。今天這篇主要說說AQS獨佔鎖的獲取。
AQS中對獨佔鎖的獲取一共有三個方法,今天主要說第一個
-
acquire:不響應中斷獲取獨佔鎖
-
acquireInterruptibly:響應中斷獲取獨佔鎖
-
tryAcquireNanos:響應中斷+超時獲取獨佔鎖
acquire方法,即在獨佔模式下獲取鎖,並且忽略中斷。它至少調用一次tryAcquire方法去獲取鎖,如果成功則直接返回,否則線程將被包裝成節點(即AQS內部類Node) 進入同步隊列,並且其可能反覆阻塞和解除阻塞,並調用tryAcquire去獲取鎖,直到最後成功。
上面這段話詳細介紹了acquire方法的執行過程,如果不理解沒關係,等看完下面的源碼解讀後,一切就清晰了
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
上面的源碼不太直觀,也不方便我後續講解,所以在不改變邏輯的前提下我將源碼改寫如下:
public final void acquire(int arg) {
if (tryAcquire(arg)) { return; }
Node node = addWaiter((Node.EXCLUSIVE), arg)
if (acquireQueued(node)
selfInterrupt();
}
我們可以看到,就四個方法,看上去是不是很簡單,所以接下來我們就將依次解讀上面四個方法
1 tryAcquire
我在"併發三板斧"已經說過,tryAcquire方法是AQS中的模板方法,是需要子類重寫實現的,其主要功能就是獲取鎖。當我們獲取到鎖時,返回true,acquire方法就直接return結束了;如果沒拿到鎖,返回false,調用入隊方法addWaiter。下面是tryAcquire的源碼
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
我們需要注意的是tryAcquire並不是一個抽象方法,而是拋了一個不支持運行的異常,這是爲什麼呢?其實很簡單,還記得"併發三板斧"文章中說的,不同的模式只需要重寫特定的方法嗎?繼承AQS的子類並不是所有的基本方法都需要重寫,而是按需重寫,如果基本方法都定義成抽象方法,則我們在實現AQS子類時就要重寫一些並不需要用到的方法
2 addWaiter
當獲取鎖失敗後,我們就會調用此方法,此方法主要功能是將獲取鎖失敗的線程包裝成Node放入等待隊列中,即入隊操作(ps:等待隊列是FIFO隊列,出隊在head端,入隊在tail端),下面是源碼
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 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;
}
我們首先會將當前線程和Node.EXCLUSIVE標誌位構造成一個Node。然後將pre指向尾結點tail,然後判斷pre是否爲null
如果爲null,說明同步隊列未初始化,則此時是沒有Node(準確的說是Node裏的線程,後面統一用Node來表示)在等待鎖,則我們會調用enq方法進行隊列初始化,然後重新入隊
如果不爲null,說明隊列不爲空,則我們就會進入第一個if分支內,嘗試將節點放入同步隊列的隊尾
首先,node.prev = pred 我們將要入隊的Node的前驅Node指向原來的尾結點pred,此時的隊列結構圖如下:
真的是如上圖所示嗎?不一定哈,上圖是在沒有併發的情況下,如果在併發的情況下,則可能如下圖所示:
此時可能有幾個節點都在進行入隊,且都走到了node.prev = pred 這一步,將自己的前驅節點指向了尾結點pred,所以下面就到了CAS發揮作用的時候啦
compareAndSetTail(pred, node)
此時只有一個節點會操作成功,我們假設中間的Node成功執行了這個操作,則此時變爲
則中間這個Node持有的線程會進入到第二個if分支內,完成Node入隊的剩餘操作,即pred的後繼Node指向tail,然後返回此Node。最上和最下這兩個操作失敗的節點則不會進入if分支內,而是調用enq方法,進行重新入隊操作
2.1 enq
在addWaiter方法解讀中我們看到了,有兩種情況我們會進入enq方法,第一種是同步隊列未初始化,第二種是在併發情況下入隊失敗。我們來看源碼
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 隊列爲空, 初始化隊列操作,即將head和tail指向一個空節點
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 隊列不爲空
// 併發下,cas操作可能會失敗,所以通過for循環不斷j進行入隊,直到成功爲止
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
if分支是處理隊列爲空的情況,即初始化隊列
else分支是處理入隊失敗的情況,這個入隊和addWaiter中的入隊是一模一樣的,所以也是會失敗的,但是我們可以看到,這裏有個for循環自旋,所以當我們入隊失敗後會再次嘗試,一直到入隊成功
這裏還需要特別注意的是,enq返回的是入隊Node的前驅節點,這裏大家有個印象就行了,這裏並沒涉及這個注意點,因爲addAwaiter方法中調用enq是沒有接受返回值的
3 acquireQueued
我們通過addWaiter入隊成功後,就會調用此方法,此方法的主要功能是掛起剛入隊Node中的線程,然後等待被喚醒再去獲取鎖。但需要注意的是,在掛起線程之前,如果滿足一定條件,此線程還會再次去獲取鎖,失敗後才掛起線程。滿足的是什麼條件呢?我們通過源碼來分析
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;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
acquireQueued方法中有兩個標誌變量:failed和interrupted,他們分別代表拿鎖失敗標誌和線程被中斷標誌,兩者爲true時分別代表拿鎖失敗和中斷成功。
我們首先會將這兩個標誌位初始化,然後我們就會進入for循環自旋。首先我們會找到入隊Node的前驅Node,然後進入第一個if判斷:判斷前驅Node是否是頭結點head,如果是,則說明入隊的Node在同步隊列的第一個,前面沒有等待的Node了,此時我們會調用tryAcquire方法來再一次獲取鎖,獲取成功後,我們進入第一個if分支中(現在大家應該知道在什麼情況下會再次去嘗試拿鎖了吧)
第一個if分支中的主要操作是更新head,即將head指向此時的Node,然後將Node中的前驅Node和線程置爲null,使得此Node變爲一個虛擬頭結點 ,最後再更新兩個標誌位並返回interrupted
如果此入隊Node的前驅Node不是head或者在第一個if判斷中拿鎖失敗,我們就會進入第二個if判斷,第二個if判斷中我們會先執行shouldParkAfterFailedAcquire方法
3.1 shouldParkAfterFailedAcquire
看這個方法名也能明白其功能:檢查Node中的線程是否需要被掛起,如果返回true則說明需要掛起,然後執行後續掛起方法parkAndCheckInterrupt,否則重新自旋。我們需要注意兩點
-
走到這個方法的線程,都已經調用tryAcquire一次或多次失敗了
-
此方法不僅僅判斷線程能否被掛起,它還有將同步隊列中屬性爲CANCELLED的Node移除隊列的功能
我們看源碼:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
"併發三板斧"中我們說過Node一共有五種狀態,其中獨佔模式下會使用三種狀態:CANCELLED、SIGNAL、初始狀態0,我們可以看到上面的代碼也是三段分支。首先我們會拿到此節點的前驅節點狀態,爲什麼是前驅Node的狀態?因爲在獨佔模式下,Node是否能夠被掛起的依據是它前驅節點是否爲SIGNAL,爲SIGNAL時才能被掛起
-
前驅節點狀態爲SIGNAL,直接返回true
-
前驅節點狀態爲CANCELLED(ws>0),則移除這些節點,返回false
-
其他情況則將前驅節點的狀態改爲SIGNAL,返回false
我們看到,只有當前驅節點爲SIGNAL才返回true;其餘情況都返回false,然後回到acquireQueued方法中自旋重新執行
3.2 parkAndCheckInterrupt
如果shouldParkAfterFailedAcquire返回ture,我們則通過parkAndCheckInterrupt方法來掛起線程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
這裏我們需要注意,當執行完LockSupport.park(this)後,此線程就被掛起了,除非當其他線程調用LockSupport.unpark喚醒當前線程或者當前線程被中斷,否則後面代碼是不會執行的
假設此時線程被喚醒了,我們此時是不知道線程是被unpark方法還是中斷喚醒的,所以我們需要通過Thread類的interrupted方法來判斷。interrupted方法會返回給我們當前線程的中斷標誌位,並將中斷標誌位復位,即置爲false。如果我們是中斷喚醒的,則返回true,然後會進入acquireQueued的第二個If分支中將interrupted置爲true。然後再次進入for循環自旋,看是獲取鎖還是又被掛起。
最後acquiredQueued方法只會存在兩種情況,第一種是獲取鎖然後返回interrupted標誌位,第二種出現異常,執行finally中if分支的cancelAcquire方法(注意,獲取鎖成功是不會執行cancelAcquire的,因爲failed標誌位爲false)
4 selfInterrupt
我們最後返回到acquire方法,如果acquire返回的是true,說明Node是被中斷喚醒的,則會調用selfInterrupt方法再一次調用中斷
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
爲啥還要執行中斷呢?還記得文章開頭我們是怎麼介紹acquire這個方法的嗎?acquire方法是在獨佔模式下不響應中斷獲取鎖的方法。如果在parkAndCheckInterrupt方法中線程是被中斷喚醒的,我們還是會繼續回到acquiredQueued中去搶鎖然後執行
當然interrupte這個方法也只是將當先線程的中斷標誌置爲true,至於會不會被中斷,我們也不知道
獨佔鎖的獲取我們就講完了,最後我再將文章的脈絡梳理下:
(未完)
歡迎大家關注我的公衆號 “程序員進階之路”,裏面記錄了一個非科班程序員的成長之路