鎖:AbstractQueuedSynchronizer源碼解析(下)

1.釋放鎖

釋放鎖是在顯式調用Lock.unlock()方法時觸發的,目的是讓線程釋放對資源的訪問權。unlock方法的基礎方法是releasereleaseShared,分別代表排它鎖和共享鎖的釋放。

釋放排它鎖—release方法

排它鎖的釋放過程比較簡單,分爲兩步

  1. 首先嚐試tryRelease方法釋放鎖,如果失敗會返回 false;反之,如果成功則執行步驟2
  2. 判斷同步隊列中是否還有節點在等待,如果有,此時當前節點一定是同步隊列的頭節點。調用unparkedSuccessor方法喚醒同步隊列中的一個後繼節點

具體源碼如下,

public final boolean release(int arg) {
    // tryRelease 交給實現類去實現,一般就是用當前同步器狀態減去arg
    // 如果返回 true 說明成功釋放鎖。
    if (tryRelease(arg)) {
        Node h = head;
        // 說明見下方
        if (h != null && h.waitStatus != 0)
            // 從同步隊列頭開始喚醒等待鎖的節點
            unparkSuccessor(h);
        return true;
    }
    return false;
}

對上面的判斷if (h != null && h.waitStatus != 0)進行說明,
3. 頭節點爲空說明同步隊列尚未初始化
4. 若頭節點的節點狀態爲0,說明是在enq方法中compareAndSetHead(new Node())初始化的頭節點。這種情況可能是其他節點加入同步隊列後,由於一些原因取消了,被從同步隊列中清除,導致隊列中只有一個waitStatus=0的節點

大部分情況下,當節點指向的線程釋放鎖時,該節點應該是同步隊列的頭節點。但是也有特殊情況,這是由於acquire方法中的操作造成的。上面兩種情況都說明,同步隊列是空的。這種情況下,無需進行後續節點的喚醒操作。

unparkSuccessor方法

unparkSuccessor方法是的作用是喚醒同步隊列中下一個節點,

private void unparkSuccessor(Node node) {
    // node節點是當前釋放鎖的節點,也是同步隊列的頭節點
    int ws = node.waitStatus;
    // 節點未被取消了,把節點的狀態置爲初始化
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 拿出 node 節點的後面一個節點
    Node s = node.next;
    // 判斷說明見下方
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 從同步隊列尾開始迭代,具體原因見下方
        for (Node t = tail; t != null && t != node; t = t.prev)
            // t.waitStatus <= 0 說明 t 沒有被取消,肯定還在等待被喚醒
            if (t.waitStatus <= 0)
                s = t;
    }
    // 喚醒最靠近同步隊列隊頭的狀態不爲CANCELLED的節點
    if (s != null)
        LockSupport.unpark(s.thread);
}

上方代碼中的判斷if (s == null || s.waitStatus > 0)

  1. s 爲空,表示 node 的後一個節點爲空
  2. s.waitStatus 大於0,代表 s 節點已經被取消了

遇到以上這兩種情況,就從隊尾開始,向前遍歷,找到最靠近隊頭的一個 waitStatus 字段不是被取消的節點對象。

尋找最靠近同步隊列頭部的狀態不爲cancelled的節點的過程是從同步隊列尾部開始的,具體原因如下,

  • 主要是因爲節點被阻塞的時候,是在 acquireQueued 方法的for循環中被阻塞的,喚醒時也一定會在 acquireQueued 方法的for循環裏面被喚醒
  • 喚醒之後會進入新一輪循環,在循環中會判斷當前節點的前置節點是否是頭節點。
  • 從尾到頭的迭代順序目的就是爲了過濾掉無效的前置節點,不然節點被喚醒時,發現其前置節點還是無效節點,就又會陷入阻塞。

acquireQueued方法過程說明見博文

獲取頭節點的後繼節點,當後繼節點的時候會調用LookSupport.unpark()方法,該方法會喚醒該節點的後繼節點所包裝的線程。因此,每一次鎖釋放後就會喚醒隊列中該節點的後繼節點所引用的線程,從而進一步可以佐證獲得鎖的過程是一個FIFO(先進先出)的過程

共享鎖釋放—releaseShared方法

釋放共享鎖分爲兩步,

  1. 首先嚐試tryReleaseShared釋放當前共享鎖,失敗會返回 false,如果成功釋放,則執行步驟2
  2. 調用doReleaseShared方法喚醒後繼節點

源碼如下,

// 共享模式下,釋放當前線程的共享鎖
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        // 這個方法就是線程在獲得鎖時,喚醒後續節點時調用的方法
        doReleaseShared();
        return true;
    }
    return false;
}

doReleaseShared方法

該方法跟獨佔式鎖釋放過程有點點不同,在共享式鎖的釋放過程中,對於能夠支持多個線程同時訪問的併發組件,必須保證多個線程能夠安全的釋放同步狀態,這裏採用的CAS保證,當CAS操作失敗continue,在下一次循環中進行重試。

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))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;  
        }
        if (h == head)  
            break;
    }
}

2. 條件隊列重要方法

首先展示條件隊列的結構,單向鏈表,
image
之前synchronized代碼中,利用Object的方式實際上是指在對象Object對象監視器上只能擁有一個同步隊列和一個等待隊列。而併發包中的Lock擁有一個同步隊列和多個條件隊列。示意圖如
image
ConditionObject是AQS的內部類,因此每個ConditionObject能夠訪問到AQS提供的方法,相當於每個Condition都擁有所屬同步器的引用。

進入條件隊列—await方法

當調用condition.await()方法後會使得當前獲取lock的線程進入到條件隊列,如果該線程能夠從await()方法返回的話一定是該線程獲取了與condition相關聯的lock

await方法源碼如下,

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 加入到條件隊列的隊尾
    Node node = addConditionWaiter();
    // 完全釋放所佔用資源,說明見下方
    int savedState = fullyRelease(node);
    
    int interruptMode = 0;
    // 確認node不在同步隊列上,再阻塞,如果 node 在同步隊列上,是不能夠上鎖的
    while (!isOnSyncQueue(node)) {
        // 阻塞在條件隊列上
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    // 能走到這裏說明節點已經從條件隊列中取出放入到同步隊列
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) 
        // 如果狀態不是CONDITION,就會自動刪除
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

await方法的具體流程,

  1. 調用addConditionWaiter方法將當前線程包裝成Node,插入到條件隊列的隊尾
  2. 釋放當前線程所佔用的lock,在釋放的過程中會喚醒同步隊列中的下一個節點
  3. 進入 while循環,判斷當前線程的節點不在同步隊列中,之後阻塞。阻塞是在 while循環中發生的,所以喚醒後也是在while循環中
  4. 當節點被 signalsignalAll方法從條件隊列中調回同步隊列後會跳出while循環並調用 acquireQueued方法獲取lock

1)addConditionWaiter方法

該方法將線程包裝爲節點插入到條件隊列的隊尾,源碼如下,

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果尾部的 waiter 不是 CONDITION 狀態了,刪除
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 新建條件隊列 node
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 隊列是空的,直接放到隊列頭
    if (t == null)
        firstWaiter = node;
    // 隊列不爲空,直接到隊列尾部
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

該方法返回的是包裝後的Node對象。

該方法中,在將節點接到隊尾之前會先判斷條件隊列尾節點的狀態,如果不是Node.CONDITION就會調用unlinkCancelledWaiters方法刪除條件隊列中所有狀態不爲Node.CONDITION的節點。

同時可以看出條件隊列是一個不帶頭結點的鏈式隊列,之前學習AQS時知道同步隊列是一個帶頭結點的鏈式隊列,這是兩者的一個區別。

2)unlinkCancelledWaiters方法

該方法從條件隊列頭部開始,刪除掉所有狀態不對的節點,源碼如下,

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    // trail 表示上一個狀態,這個字段作用非常大,可以把狀態都是 CONDITION 的 node 串聯起來,即使 node 之間有其他節點都可以
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        // 當前node的狀態不是CONDITION,刪除自己
        if (t.waitStatus != Node.CONDITION) {
            //刪除當前node
            t.nextWaiter = null;
            // 如果 trail 是空的,循環又是從頭開始的,說明從頭到當前節點的狀態都不是 CONDITION
            // 都已經被刪除了,所以移動隊列頭節點到當前節點的下一個節點
            if (trail == null)
                firstWaiter = next;
            // 如果找到上次狀態是CONDITION的節點的話,先把當前節點刪掉,然後把自己掛到上一個狀態是 CONDITION 的節點上
            else
                trail.nextWaiter = next;
            // 遍歷結束,最後一次找到的CONDITION節點就是尾節點
            if (next == null)
                lastWaiter = trail;
        }
        // 狀態是 CONDITION 的 Node
        else
            trail = t;
        // 繼續循環,循環順序從頭到尾
        t = next;
    }
}

流程示意圖如下,
image

3)fullyRelease方法

當前節點插入到等待對列之後,會使當前線程釋放lock,由fullyRelease方法實現,源碼如下,

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;
    }
}

調用AQS的方法release方法釋放AQS的同步狀態並且喚醒在同步隊列中頭結點的後繼節點引用的線程,如果釋放成功則正常返回,若失敗的話就拋出異常。

4)從await方法中退出

while (!isOnSyncQueue(node))退出循環的條件是節點從條件隊列中取出放回至同步隊列。目前想到的只有兩種可能,

  1. node 剛被加入到條件隊列中,立馬就被其他線程 signal 轉移到同步隊列中去了
  2. 線程之前在條件隊列中沉睡,被喚醒後加入到同步隊列中去

退出循環後直接嘗試 acquireQueued方法在同步隊列中阻塞直至獲取鎖。

線程喚醒

1)單個線程喚醒—signal方法

調用condition的signal或者signalAll方法可以將條件隊列中等待時間最長的節點移動到同步隊列中,使得該節點能夠有機會獲得lock。按照條件隊列是先進先出(FIFO)的,所以條件隊列的頭節點必然會是等待時間最長的節點,也就是每次調用condition的signal方法是將頭節點移動到同步隊列中。

該方法對應下圖的淺藍色曲線的過程,
在這裏插入圖片描述

signal方法是ConditionObject類中的方法,源碼如下,

public final void signal() {
	// 第一步檢驗
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 從頭節點開始喚醒
    Node first = firstWaiter;
    if (first != null)
        // doSignal 方法會把條件隊列中的節點轉移到同步隊列中去
        doSignal(first);
}
  1. signal方法首先會檢測調用signal方法的線程是否已經獲取lock,如果沒有獲取lock會直接拋出異常,如果獲取的話再得到等待隊列的頭指針引用的節點
  2. 之後的操作的doSignal方法也是基於該節點。

調用ConditionObject對象的signal方法的前提條件是當前線程已經獲取了lock,該方法會使得條件隊列中的頭節點,即等待時間最長的那個節點移入到同步隊列。移入到同步隊列後纔有機會使得等待線程被喚醒,即從await方法中的LockSupport.park(this)方法中返回,從而纔有機會使得調用await方法的線程成功退出

條件隊列頭節點移入同步隊列—doSignal方法

該方法源碼如下,

private void doSignal(Node first) {
    do {
        // firstWaiter指向當前節點的nextWaiter
        // 若firstWaiter爲空,說明到隊尾了
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
            
        // 從隊列頭部開始喚醒
        first.nextWaiter = null;
        
        // transferForSignal 方法會把節點轉移到同步隊列中去
        // (first = firstWaiter) = null 說明隊列中的元素已經循環完了
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
  1. doSignal方法將條件隊列頭節點的後置節點置爲 null,這種操作其實就是把 node 從條件隊列中移除。
  2. 通過 while 保證 transferForSignal方法能執行成功

喚醒過程最關鍵方法—transferForSignal

transferForSignal 方法會把節點轉移到同步隊列中去,該方法是真正對頭節點做處理的邏輯,源碼如下,

// 傳入參數 node 是條件隊列的頭節點
final boolean transferForSignal(Node node) {
    // 將 node 的狀態從 CONDITION 修改成初始化,失敗返回 false
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 當前隊列加入到同步隊列,返回的 p 是 node 加入同步隊列後的前置節點
    Node p = enq(node);
    int ws = p.waitStatus;
    
    // 狀態修改成 SIGNAL,如果成功直接返回
    // 把前置節點p的狀態改成 SIGNAL 是因爲 SIGNAL 本身就表示該節點後面的節點都是需要被喚醒的
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 如果 p 節點被取消,或者狀態不能修改成SIGNAL,直接喚醒
        LockSupport.unpark(node.thread);
    return true;
}

transferForSignal方法返回 true 表示轉移成功, false 表示轉移失敗。

2)全部線程喚醒—signalAll方法

signalAll方法的作用是喚醒條件隊列中全部節點,源碼如下,

public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 拿到頭節點
    Node first = firstWaiter;
    if (first != null)
        // 從頭節點開始喚醒條件隊列中所有的節點
        doSignalAll(first);
}

上方代碼與signal方法的區別僅在於將內部調用的方法從doSignal改爲doSignalAll

條件隊列所有節點移入同步隊列—doSignalAll方法

把條件隊列所有節點依次轉移到同步隊列去,

// 傳入參數 node 是條件隊列的頭節點
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        // 拿出條件隊列隊列頭節點的下一個節點
        Node next = first.nextWaiter;
        // 把頭節點從條件隊列中刪除
        first.nextWaiter = null;
        // 頭節點轉移到同步隊列中去
        transferForSignal(first);
        // 開始循環頭節點的下一個節點
        first = next;
    } while (first != null);
}

該方法將條件隊列中的每一個節點都移入到同步隊列中,即“通知”當前調用ConditionObject#await()方法的每一個線程

3. await與signal/signalAll的結合思考

圖示

使用 ConditionObject 提供的awaitsignal/signalAll方法就可以實現這種機制,而這種機制能夠解決最經典的問題就是“生產者與消費者問題”。

await和signal和signalAll方法就像一個開關控制着線程A(等待方)和線程B(通知方)。它們之間的關係可以用下面一個圖來表現,
在這裏插入圖片描述

  1. 線程awaitThread先通過lock.lock()方法獲取鎖成功後調用了condition.await方法進入等待隊列
  2. 另一個線程signalThread通過lock.lock()方法獲取鎖成功後調用了condition.signal或者signalAll方法,使得線程awaitThread能夠有機會移入到同步隊列中
  3. 當其他線程釋放lock後使得線程awaitThread能夠有機會獲取lock,從而使得線程awaitThread能夠從await方法中退出執行後續操作。如果awaitThread獲取lock失敗會直接進入到同步隊列。

示例

public class AwaitSignal {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        Thread waiter = new Thread(new waiter());
        waiter.start();
        Thread signaler = new Thread(new signaler());
        signaler.start();
    }

    static class waiter implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                while (!flag) {
                    System.out.println(Thread.currentThread().getName() + "當前條件不滿足等待");
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "接收到通知條件滿足");
            } finally {
                lock.unlock();
            }
        }
    }

    static class signaler implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                flag = true;
                condition.signalAll();
            } finally {
                lock.unlock();
            }
        }
    }
}

上面代碼的執行結果,

Thread-0當前條件不滿足等待
Thread-0接收到通知,條件滿足

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章