本文是學習《多處理器程式設計的藝術》的筆記。
下面主要介紹了一些常用的鎖的原理,這些只是一些理論,離我們實際使用還是有一些差距的。不過這種理論也往往是相對比較好容易理解了掌握的,只有瞭解了這些理論,可以加深我們對鎖原理知識的理解。有能力的同學更可能根據這些理論定製開發符合自己場景的高效能的鎖。
本篇文章並沒有具體講到ava中鎖的實現。但是通過本篇文章,相信大家對Java中鎖的實現也會有更深入的理解。因為Java中Lock
的底層實現AbstractQueuedSynchronizer
的原型在這裡就會講到。
同樣的這些原理在其他很多地方也能見到,比如TCP中關於碰撞的處理就和這裡提到的指數後退鎖是一樣的。zookeeper
的分散式鎖也就是通過佇列的方式來實現的。
測設-設定鎖
import java.util.concurrent.atomic.AtomicBoolean;
public class TASLock {
private AtomicBoolean state=new AtomicBoolean(false);
public void lock(){
while(state.getAndSet(true)){
}
}
public void unlock(){
state.set(false);
}
}
TASLock通過將state變數的值由false變為true來設定加鎖。
注意:只有將state變數從false變為true的執行緒,可以獲得鎖,其他執行緒都會在state.getAndSet(true)
上一直自旋
import java.util.concurrent.atomic.AtomicBoolean;
public class TTASLock {
private AtomicBoolean state=new AtomicBoolean(false);
public void lock(){
while(true){
while(state.get()){}
if(!state.getAndSet(true)){
return;
}
}
}
public void unlock(){
state.set(false);
}
}
TTASLock和TASLock基本一樣,只不過,TTASLock在設定state的值為true之前,首先會判斷state當前的值是不是false,如果不是false就會在state.getAndSet(true)
上自旋等待;如果將state的值從false設定為true失敗就又會重新開始上述操作。
TASLock和TTASLock它們的效能如何呢?
從圖上的結果就能看出TASLock和TTASLock它們直接的效能差距還是比較明顯的。
為什麼會有這麼大的效能差距呢?
主要的原因是為了CPU訪問資料的速度,CPU內部都有一個小容量的儲存器cache,CPU只會跟cache通訊,並不會直接跟記憶體通訊。
getAndSet
設定的變數是有volatile
修飾的。根據快取一致性協議,這事getAndSet
會直接將資料寫回記憶體,同時迫使其他處理器cache中的對應的副本無效,這時其他處理器就需要去記憶體中重新讀取對應變數的新值。
處理器寫回資料到記憶體、處理器從記憶體中讀取資料都是通過匯流排來完成的。是通過匯流排將資料寫到記憶體中的。而在任何時刻值只能有一個處理器佔用匯流排和記憶體通訊,其他想要讀寫記憶體的處理器會被阻塞。
這就是TASLock
效能為什麼差的原因。
再來看看TTASLock
,執行緒B第一次讀鎖時發生cache缺失,會從記憶體中讀取值到它的cache中。只要執行緒A持有鎖,執行緒B就會不斷重新該值,這時每次都是cache命中,不會佔用匯流排從記憶體中讀取,也不會降低其他執行緒的記憶體訪問速度。此外,釋放鎖的執行緒也不會被正在該鎖上自旋的執行緒所延遲。
然而,當鎖被釋放的情況並不理想。鎖的持有者將false寫入鎖變數來釋放鎖。該操作會使自旋執行緒的cache副本立刻失效。每個執行緒都將放生一次cache缺失並重新讀取新值,它們會同時呼叫getAndSet
以獲取鎖。第一個成功的執行緒將再次使其他執行緒中cache中的值失效。這些失效執行緒接下來又重新從記憶體中讀取那個值,從而引起一場匯流排流量風暴。最終,所有執行緒再此平靜,進入本地自旋。
從上面也可以看出,本地自旋每次重複讀取被快取的值而不是反覆使用匯流排從記憶體中讀取,對設計高效的自旋鎖非常關鍵。
指數後退
TTASLock類中,效能消耗主要在當鎖看似空閒時,呼叫getAndSet
來獲取鎖,這個過程極有可能存在高爭用。因為每個獲取鎖的執行緒幾乎都是同時看到鎖空閒,同時呼叫getAndSet
來獲取鎖,但是這時每個獲取鎖的執行緒獲得鎖的機會都是非常小的。所以我們可以想辦法來避免每個獲取鎖的執行緒同時呼叫getAndSet
,讓它們在不同的時間呼叫getAndSet
,給正在競爭的執行緒以結束的機會,將會更有效。
所以當執行緒呼叫getAndSet
獲取鎖失敗時,讓它們隨機暫停一段時間來減少它們的獲取鎖的爭用。暫停的時間太短,沒有意義;太長,會影響整個獲取鎖的時間。暫停時間固定的話,也沒有意義,因為下次它們又會是同時呼叫getAndSet
。所以我們需要指定最小暫停時間minDelay,最大暫停時間maxDelay。限制暫停時間在它們之間的一個隨機值。
說了這麼多了,我們直接看程式碼吧。
首先定義一個暫停時間的類Backoff
。
import java.util.Random;
public class Backoff {
final int minDelay,maxDelay;
int limit;
final Random random;
public Backoff(int min,int max){
minDelay=min;
maxDelay=min;
limit=minDelay;
random=new Random();
}
public void backoff() throws InterruptedException {
int delay=random.nextInt(limit);
limit=Math.min(maxDelay,2*limit);
Thread.sleep(delay);
}
}
這裡的暫停時間上限每次都是*2,所以也叫指數後退。
這種指數後退的處理演算法TCP的碰撞處理也用到了
下面我們修改TTASLock
類獲取鎖的getAndSet
方法中返回false
時,呼叫backoff
方法隨機暫停個時間。我們把這個修改的類叫BackoffLock
,它的完整程式碼就是下面這個樣子了
import java.util.concurrent.atomic.AtomicBoolean;
public class BackoffLock implements Lock {
private AtomicBoolean state = new AtomicBoolean(false);
private static final int MIN_DELAY = 1;
private static final int MAX_DELAY = 10;
public void lock() {
Backoff backoff = new Backoff(MIN_DELAY, MAX_DELAY);
while (true) {
while (state.get()) {
}
if (!state.getAndSet(true)) {
return;
} else {
try {
backoff.backoff();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void unlock() {
state.set(false);
}
}
BackoffLock
這種指數後退的鎖易於實現,且在許多系統結構中要比TASLock、TTASLock的效能要好。但是它的暫停時間的最優值的選擇與處理器的個數以及速度密切相關,因此,很難調整BackoffLock
類以使它與各種不同的機器相互相容。
佇列鎖
這是一種實現了可擴充套件自旋鎖的方法,比BackoffLock
這種指數後退鎖複雜一些,但卻有更好的移植性。在BackoffLock
中有兩個問題
- cache一致性流量:所有執行緒都在同一個共享儲存單元上自旋,每一次成功的鎖訪問都會產生cache一致性流量
- 臨界區利用率低:執行緒延遲過長,導致臨界區利用率低下。
可以將執行緒組織成一個佇列來克服這些缺點。在佇列中,每個執行緒檢測其前驅執行緒是否已完成來判斷是否輪到它自己。獲取鎖,釋放鎖只有當前節點的後續節點的cache失效,不會影響到其他節點;這樣就只有這個後續節點會去通過匯流排訪問記憶體,其他節點的cache依舊是有效的。
基於陣列的鎖
import java.util.concurrent.atomic.AtomicInteger;
public class ALock implements Lock{
private ThreadLocal<Integer> mySlotIndex=ThreadLocal.withInitial(()->0);
private AtomicInteger tail;
private volatile boolean[] flag;
private int size;
public ALock(int capacity){
size=capacity;
tail=new AtomicInteger(0);
flag=new boolean[capacity];
flag[0]=true;
}
public void lock(){
int slot=tail.getAndIncrement()%size;
mySlotIndex.set(slot);
while(!flag[slot]){};
}
public void unlock(){
int slot= mySlotIndex.get();
flag[slot]=false;
flag[(slot+1)%size]=true;
}
}
在上面的類中,有一個boolean型別的flag陣列(如上圖所示,上圖中的陣列大小為128),陣列中每個位置對應一個要加鎖的執行緒。只有當陣列中值為true的執行緒可以獲取鎖。有一個AtomicInteger型別的tail,每個加鎖的執行緒通過修改它的值,來獲得當前執行緒所對應的flag陣列中的位置。
tail屬性是一個成員變數,可以被多個執行緒共享,初始值是0。為了獲得鎖,每個執行緒原子地增加tail1域的值。所得的結果值作為執行緒的槽,也就是對應陣列flag的下標。同時這個結果值也通過ThreadLocal和對應的執行緒做了繫結。
如果flag[i]的值為true,那麼取得結果值為i的執行緒就有權獲得鎖。
在初始狀態時,只有flag[0]為true,其他的都是false。(java中原始變數的boolean的預設值是false)。表明只有當前只有增加tail的返回值是0的執行緒可以獲取鎖。
在釋放鎖值,執行緒把自己對應的陣列位置值設定成false,同時將陣列下一個位置的值設定成true。確保取的陣列下一個位置的執行緒可以獲得鎖。
但是這種陣列結構有兩個問題:
1 由於我們是先建立ALock的物件,這時flag陣列的容量就確定了,但是我們每個獲取鎖的執行緒都需要去通過得到資料下標的方式來獲取鎖。但是我們有多少個執行緒來獲取鎖,常常在建立ALock物件時未知的,為了避免陣列越界,我們通過取餘的方式來得到執行緒在陣列中的位置,但這就有了一個問題,多個執行緒可能會對應到一個陣列位置,也就是可能存在多個執行緒同時獲得鎖的問題。
2 從上面右邊的圖上可以看到flag陣列中多個元素在同一個快取行,也就有了偽共享的問題。
要避免偽共享的問題,那就需要每個執行緒在陣列中更新的值(tail.getAndIncrement()%size;
得到的值)都在單獨的一個快取行。換句話說也就是每個執行緒更新的值代表一個快取行。
每次獲取鎖時呼叫tail.getAndIncrement()%size;
的返回值*(一個快取行的位元組數/陣列中單個元素的位元組數)。這樣就可以確保每個執行緒修改陣列的位置都在單獨的一個快取行。
CLH佇列鎖
上面可以看到基於陣列的鎖的兩個問題:
1.存在多個執行緒可能同時獲得鎖的問題。
2.為了解決偽共享的問題,需要浪費記憶體和cache。
CLH佇列鎖就可以解決這個問題。
說到CLH,如果你對Java的鎖有一定的瞭解,那你對這個名字至少應該不會感到陌生,因為在AbstractQueuedSynchronizer
中的佇列就是參考CLH佇列來實現的。
CLH佇列鎖是一個單向連結串列結構,每個節點都有一個指向它前驅節點的引用。每個節點的初始值都是true。當它的前驅節點的值是false時,代表它的前驅節點釋放了鎖,當前節點可以獲得鎖。
下面看看CLH佇列鎖的實現程式碼:
public class CLHLock {
private AtomicReference<QNode> tail = new AtomicReference<QNode>(new QNode());
ThreadLocal<QNode> myPred;
ThreadLocal<QNode> myNode;
public CLHLock() {
tail = new AtomicReference<>(new QNode());
myNode = ThreadLocal.withInitial(() -> new QNode());
myPred = ThreadLocal.withInitial(() -> null);
}
public void lock() {
QNode qNode = myNode.get();
qNode.locked = true;
QNode pred = tail.getAndSet(qNode);
myPred.set(pred);
while (pred.locked) {
}
}
public void unlock() {
QNode qNode = myNode.get();
qNode.locked = false;
}
privaet static class QNode {
public volatile boolean locked;
}
}
佇列的每個節點都是一個QNode。這種其實是一種隱式佇列。因為它的佇列節點QNode並沒有屬性指向其前驅節點或者後續節點。
myPred 和 myNode都是ThreadLocal變數。它們分別儲存佇列中每個執行緒的在佇列上的前驅節點和它自己的節點。
它的前後節點是這樣串起來的:
13行首先
myNode.get();
返回myNode
這個ThreadLocal變數儲存的節點。每個執行緒加鎖的時候都會呼叫
tail.getAndSet(qNode)
,將佇列中的尾節點tail替換成第1步得到的節點``myPred.set(pred);
這句程式碼將原來的tail節點儲存到
myPred`這個ThreadLocal變數中。這裡執行完之後,原來的tail節點就儲存到
myPred
中,而新的tail節點就是myNode
儲存的節點。原來的tail節點也就成了新的tail節點的前驅節點。這裡需要注意的是就算有多個執行緒同時呼叫
tail.getAndSet(qNode)
,這行程式碼也是序列執行的。它們只會修改最新的tail節點。也就是隻會影響到myNode儲存的節點是不是當前最新的tail節點。並不會影響到myNode儲存的節點和myNode儲存的節點的關係。也就是說不會改變myNode儲存的節點是myNode儲存的節點的前驅節點這個事實。tail代表佇列的尾節點。初始時,佇列中只有一個tail節點。
在加鎖過程中,每個執行緒通過將自己新增到tail節點後面,將自己的節點(myNode)作為tail節點的值,同時將原來的tail節點的值更新尾當前執行緒的前驅節點(myPred)的值。通過這種方式,當前執行緒就將自己的節點加入到了佇列中。它去觀察它前驅節點(myPred)的值,當myPred節點的值為false時,表明當前執行緒可以獲得鎖。
在解鎖的過程中,只需要將當前執行緒節點(myNode)的值更新為false,它的後續執行緒就可以獲得鎖。
初始狀態:
這時,只有一個tail節點。它的值false。
加鎖階段:
首先會建立一個當前節點myNode,將它的值設定為True。
同時呼叫tail.getAndSet(qNode)
將自己更新為tail節點,它的返回值pred是原來的tail節點。
再執行myPred.set(pred);
將原來的tail節點設定成當前節點的前驅節點。
當前節點通過自旋的方式去觀察前驅節點的值是否為false,只有當前驅節點為false時,表示前驅節點已經釋放鎖,當前節點可以獲得鎖。
解鎖階段:
這裡只需要將當前節點myNode的值設定成false就可以了。
當前執行緒結束後ThreadLocal變數也會自動被垃圾回收器回收。這裡的myPred節點由於沒有其他引用,就會被垃圾回收器回收。myNode節點要不就是tail節點,要不就是它的後續節點的前驅節點,所以myNode節點並不會被垃圾回收器回收。
CLH佇列鎖可以解決ALock的兩個問題。
1.由於我們當前時一個連結串列,所以不存在容量不夠的問題。
2.我們CLH佇列中的每個節點都是在呼叫加鎖階段才建立的。而且由於每個節點都是由不同執行緒建立的。所以我可以認為它們不會在同一個快取行中。
但是CLH佇列鎖也有一個問題。
1.由於每個執行緒都要去觀察它的前驅節點,進行自旋。所以在無cache的NUMA系統結構下,就會出現前驅節點可能在其他CPU記憶體上,效能就會比較差。
MCS佇列鎖:
MCS佇列鎖也被標識為QNode物件的連結串列,其中的每個QNode要麼表示一個鎖的持有者,要麼表示一個正在等待獲得鎖的執行緒。與CLHLock佇列鎖相比,它的連結串列時顯式的而不是虛擬的:整個連結串列通過QNode物件裡的next屬性作為後續節點的引用,而在CLH佇列鎖中時通過ThreadLocal變數myPred來指向前驅節點的。
下面我們看看MCS佇列鎖的程式碼:
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
public class MCSLock implements Lock {
private final AtomicReference<QNode> queue;
private AtomicReference<QNode> tail = new AtomicReference<QNode>(new QNode());
ThreadLocal<QNode> myNode;
public MCSLock(){
queue=new AtomicReference<QNode>();
myNode=ThreadLocal.withInitial(()->new QNode());
}
@Override
public void lock() {
QNode qNode=myNode.get();
QNode pred=tail.getAndSet(qNode);
if(Objects.nonNull(pred)){
qNode.locked=true;
pred.next=qNode;
while(qNode.locked){
}
}
}
@Override
public void unlock() {
QNode qNode=myNode.get();
if(Objects.isNull(qNode.next)){
if(tail.compareAndSet(qNode,null)){
return ;
}
while(Objects.isNull(qNode.next)){
}
}
qNode.next.locked=false;
qNode.next=null;
}
private static class QNode {
public volatile boolean locked;
public volatile QNode next;
}
}
加鎖階段:
每個執行緒的 節點還是呼叫
tail.getAndSet(qNode)
將自己新增到整個佇列的尾部,同時修改自己為tail節點。它的返回值就是原來tail儲存的節點。由於在初始化時未給tail分配節點(,所以tail的值有可能為空unlock也可能導致tail節點為空)。接下來判斷原來的tail節點是否為空:
1.如果為空,表示當前節點就是整個佇列的頭節點。這時就可以獲取鎖。
2.如果原來的tail節點不為空,就將當前節點的locked屬性設定為true,同時將原來的tail節點的next屬性設定為當前節點。
在當前節點上自旋,等待當前節點的locked屬性為false,以便獲取鎖。
解鎖階段:
首先判斷當前節點的後續節點next屬性是否為空,如果為空,那就有兩種情況。
- 當前節點就是隊尾節點。
- 已經有了後續節點,但是後續節點還沒有呼叫
pred.next=qNode;
將當前要釋放鎖的節點的後續節點設定成它自己。對於這兩種情況,都會通過呼叫
tail.compareAndSet(qNode,null)
來判斷當前節點是否是tail節點,並將tail節點由當前節點設定成null,然後直接返回。如果這句能執行成功,那就說明當前節點就是隊尾節點,也就是在當前解鎖的執行緒執行完tail.getAndSet(qNode)
這就後,一直沒有其他執行緒執行過這行程式碼。這也就是解決上面next為空的第一種場景。如果
tail.compareAndSet(qNode,null)
這句執行不成功,說明已經有其他執行緒執行了tail.getAndSet(qNode)
這句程式碼,當前的tail節點已經不是當前執行緒自己的節點了 。這時就需要等待執行完tail.getAndSet(qNode)
程式碼的執行緒直到它執行完pred.next=qNode;
這句程式碼,這裡是通過自旋進行等待的。這也是解決上面說的第二種場景。上面的第一種場景 在34行已經return,程式碼能執行到40行,就表示當前節點已經不是隊尾tail節點了。這時只需要將當前節點的後續節點next節點的locked值設定為false,從而喚醒next節點獲得鎖。
qNode.next=null;
將後續節點和當前節點的連結斷開,方便當前節點被垃圾回收器回收。
對比CLH佇列鎖,MCS佇列鎖在加鎖階段只是在自己的節點上自旋,也就是沒有了無cache的NUMA問題。但是它解鎖的時候也需要在後續節點上進行自旋,不過這個自旋的時候就非常短,無cache的NUMA問題的問題基本也可以忽略吧。
時限佇列鎖
Java的Lock介面中有個boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
方法,呼叫者可以指定一個時限:呼叫這個方法為獲得鎖而準備等待的最大時間,如果在呼叫者獲得鎖之前超時,呼叫者則放棄獲得鎖的嘗試。
用CLH佇列鎖來舉例,我們可以過載lock方法,傳入最大的等待時間,執行緒在它的前驅節點自旋的時候判斷是否超時,如果超時就返回。
public void lock(long time) {
long start=System.currentTimeMillis();
QNode qNode = myNode.get();
qNode.locked = true;
QNode pred = tail.getAndSet(qNode);
myPred.set(pred);
while (pred.locked) {
//超時
if((System.currentTimeMillis()-start)>time){
return;
}
}
}
上面的程式碼是我自己寫的,書上沒有。上面的程式碼只是在超時時直接return,看著和正常獲取鎖的基本沒區別。嚴格來說,在超時時,我們還需要將當前節點的前驅節點和當前節點的後續節點連起來,從整個佇列中刪除掉當前節點。這裡有兩個問題:
1.CLH佇列鎖都只有前驅節點的引用,沒有後續節點的引用。所以如果要刪除當前節點,讓當前節點的前驅節點和後續節點連起來,就需要每個節點判斷它的前驅節點是不是由於超時等原因實效了,如果失效了 就要將它的前驅節點剔除掉,將它的前驅節點設定成它前驅節點的前驅節點。這個無疑增加了加鎖節點程式碼的複雜度。
2.從一個佇列節點中刪除一個節點還會擾亂併發鎖的釋放,這個解決起來也會比較困難。
所以我們需要採用一種迂迴的方法,不是在超時時主動的將當前節點從佇列中刪除掉,而是採用惰性方法:若一個執行緒超時,則該執行緒將它的節點標記為已放棄。這樣該執行緒在佇列中的後續(如果有)將會注意到它正在自旋的節點已經被放棄,於是開始在被放棄節點的前驅上自旋。
下面還是直接看程式碼吧
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class TOLock implements Lock {
static QNode AVAILABLE = new QNode();
AtomicReference<QNode> tail;
ThreadLocal<QNode> myNode;
public TOLock() {
tail = new AtomicReference<>();
myNode = ThreadLocal.withInitial(() -> new QNode());
}
@Override
public void lock() {
}
@Override
public boolean tryLock(long time, TimeUnit unit) {
long startTime = System.currentTimeMillis();
long patience = TimeUnit.MILLISECONDS.convert(time, unit);
QNode qNode = new QNode();
myNode.set(qNode);
qNode.pred = null;
QNode myPred = tail.getAndSet(qNode);
if (Objects.isNull(myPred) || myPred.pred == AVAILABLE) {
return true;
}
while (System.currentTimeMillis() - startTime < patience) {
QNode predPred = myPred.pred;
if (predPred == AVAILABLE) {
return true;
} else if (Objects.nonNull(predPred)) {
myPred = predPred;
}
}
if (!tail.compareAndSet(qNode, myPred)) {
qNode.pred = myPred;
}
return false;
}
@Override
public void unlock() {
QNode qNode = myNode.get();
if (!tail.compareAndSet(qNode, null)) {
qNode.pred = AVAILABLE;
}
}
private static class QNode {
public volatile QNode pred = null;
}
下面我們簡單分析下上面的程式碼
首先定義了一個
QNode
物件(57行),作為當前執行緒加鎖狀態的節點,其中它有個pred
屬性(58行)。關於pred
的取值,分為下面幾種情況:
- 當前執行緒已經獲得了鎖,或者當前執行緒正在獲取鎖(此時還沒有超時),這時
pred
的值就是null
。- 如果當前執行緒在獲取鎖的過程中超時了,這時
pred
的值就是它的前驅節點的pred
的值。- 如果當前執行緒解鎖時,佇列中已經有了後續執行緒,就將
pred
的值設定成AVAILABLE
,指示它的後續節點可以獲得鎖。加鎖的程式碼:
1.首先我們建立一個
QNode
物件,儲存到threadLocal
變數myNode
中。然後通過原子的方式將佇列中的tail
尾節點更新為當前執行緒的QNode
物件(28行),同時將原來的tail
節點儲存到myPred
變數中。2.接下來判斷
myPred
的值: (1).如果
myPred==null
,表示當前佇列中只有當前執行緒一個節點,此時它就可以獲取到鎖。 (2).如果
myPred.pred == AVAILABLE
,表示當前節點的前驅節點已經釋放了鎖,此時當前執行緒也就可以獲取到鎖。 (3).對於其他的情況,都不能直接獲取鎖。
3.在32行開始,使用
while
迴圈來等待它的前驅節點釋放鎖或者超時。 在這裡面主要是通過對
myPred
變數的pred
值進行判斷。在進入while
迴圈時,myPred
表示當前執行緒的前驅節點。前面已經說過pred
的取值有3中情況,下面我們對while
迴圈中pred
取值的3中情況簡單分析下:
QNode predPred = myPred.pred;
由於myPred
是當前執行緒的前驅節點,所以這裡的predPred
就是當前執行緒的前驅節點的pred屬性。 (1).
predPred==null
,表示當前執行緒的前驅節點也在等待獲取鎖,這時當前執行緒需要繼續在自己的節點上迴圈,等待前驅節點超時(36行),或者前驅節點獲得鎖後並釋放鎖(34行)。 (2).
predPred==AVAILABLE
表示前驅節點已經釋放了鎖,當前節點就可以獲取鎖。 (3)其他取值,這種其實也就只有一種取值了,就是42行的程式碼。不過這裡需要注意的是42行是當前執行緒節點超時的操作。
while
迴圈中這裡就是當前執行緒的前驅節點超時的情況了。也就是36行的程式碼,直接修改myPred = predPred;
跳過超時的節點,繼續 執行後續操作。4.在41行,對超時進行一個處理。如果當前執行緒的節點依舊是佇列中的尾節點(在28行到41行之間,沒有其他執行緒執行第28行程式碼),就直接將當前節點從佇列中去掉。如果當前執行緒的節點已經不是尾節點,就將當前執行緒節點的
pred
的前驅節點。解鎖的程式碼
這個就比較簡單了,直接判斷佇列中是否有後續節點,如果有後續節點,就將當前節點的
pred
設定成AVAILABLE
,喚醒佇列中的後續節點。