JUC--AQS源碼分析(三)阻塞和喚醒線程

1 概述

上一篇文章 JUC--AQS源碼分析(二)同步狀態的獲取與釋放,我們學習到了同步狀態的獲取與釋放的源碼,並且對線程的阻塞和喚醒有了一個初步的瞭解,這裏我們進行深一步的分析。

2 阻塞

我們知道在獲取線程同步狀態失敗的時候,會將線程加入到CLH同步隊列,並且進行自旋等待。而在自旋等待方法acquireQueued中我們可以看見需要再次進行獲取同步狀態,如果獲取同步狀態失敗則需要判斷當前線程是否能夠被阻塞,並且進行線程阻塞後檢測中斷狀態。

//如果線程能夠被阻塞就阻塞線程並且返回線程的中斷狀態。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;

接下來我們來學習shouldParkAfterFailedAcquire和parkAndCheckInterrupt着兩個方法到底是怎麼回事。

2.1 shouldParkAfterFailedAcquire判斷是否阻塞

首先直接上源碼,進行源碼分析。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        
        //前去節點狀態
        int ws = pred.waitStatus;

        //狀態爲SIGNAL,表示當前節點處於等待狀態,直接返回true。
        if (ws == Node.SIGNAL)
            return true;

        //狀態大於0,則爲CANCEL,表示該節點已經超時,或者被中斷,需要CLH隊列中刪除。
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;

        //前驅節點狀態爲CONDITION、PROPAGATE
        } else {
            

            //CAS設置狀態爲SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

上面這段代碼主要的作用就是檢查當前線程是否需要被阻塞,具體規則如下:

(1)如果前驅節點狀態爲SIGNAL,則表明當前線程需要被阻塞,則直接返回true。

(2)如果前驅節點狀態爲CANCEL,則表明該節點的前驅節點已經超時或者被中斷,需要從同步隊列中刪除,直到回溯到前驅節點的狀態<=0,返回false。

(3)如果前驅節點非SIGNAL,非CANCEL,則通過CAS方式將前驅節點設置成SIGNAL,返回false。

如果shouldParkAfterFailedAcquire返回true,則調用parkAndCheckInterrupt阻塞線程。

2.2 parkAndCheckInterrupt阻塞線程 

首先還是直接上源碼。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

從上面的源碼我們可以看出parkAndCheckInterrupt做的事就相對簡單多了,直接阻塞當前線程,並且返回一個線程中斷狀態就行了。

3 喚醒

當線程釋放同步狀態後,就需要喚醒該線程的後繼節點。調用unparkSuccessor方法。

private void unparkSuccessor(Node node) {
        //當前節點狀態
        int ws = node.waitStatus;
        //當前狀態 < 0 則設置爲 0,刪除當前節點
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        //當前節點的後繼節點
        Node s = node.next;
        //後繼節點爲null或者其狀態 > 0 (超時或者被中斷了)
        if (s == null || s.waitStatus > 0) {
            s = null;
            //從tail節點來找可用節點
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //喚醒後繼節點
        if (s != null)
            LockSupport.unpark(s.thread);
    }

爲何是從tail尾節點開始,而不是從node.next開始呢?由於node.next仍然可能會存在null或者取消了,所以採用tail回溯辦法找第一個可用的線程。

上面就是對AQS阻塞和喚醒線程的學習,後面用一篇博文學習LockSupport。

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