Java併發包源碼學習系列:CLH同步隊列及同步資源獲取與釋放

本篇學習目標

  • 回顧CLH同步隊列的結構。
  • 學習獨佔式資源獲取和釋放的流程。

CLH隊列的結構

我在Java併發包源碼學習系列:AbstractQueuedSynchronizer#同步隊列與Node節點已經粗略地介紹了一下CLH的結構,本篇主要解析該同步隊列的相關操作,因此在這邊再回顧一下:

AQS通過內置的FIFO同步雙向隊列來完成資源獲取線程的排隊工作,內部通過節點head【實際上是虛擬節點,真正的第一個線程在head.next的位置】和tail記錄隊首和隊尾元素,隊列元素類型爲Node。

  • 如果當前線程獲取同步狀態失敗(鎖)時,AQS 則會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程
  • 當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。

接下來將要通過AQS以獨佔式的獲取和釋放資源的具體案例來詳解內置CLH阻塞隊列的工作流程,接着往下看吧。

資源獲取

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && // tryAcquire由子類實現,表示獲取鎖,如果成功,這個方法直接返回了
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果獲取失敗,執行
            selfInterrupt();
    }
  • tryAcquire(int)是AQS提供給子類實現的鉤子方法,子類可以自定義實現獨佔式獲取資源的方式,獲取成功則返回true,失敗則返回false。
  • 如果tryAcquire方法獲取資源成功就直接返回了,失敗的化就會執行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))的邏輯,我們可以將其進行拆分,分爲兩步:
    • addWaiter(Node.EXCLUSIVE):將該線程包裝成爲獨佔式的節點,加入隊列中。
    • acquireQueued(node,arg):如果當前節點是等待節點的第一個,即head.next,就嘗試獲取資源。如果該方法返回true,則會進入selfInterrupt()的邏輯,進行阻塞。

接下來我們分別來看看addWaiteracquireQueued兩個方法。

入隊Node addWaiter(Node mode)

根據傳入的mode參數決定獨佔或共享模式,爲當前線程創建節點,併入隊。

    // 其實就是把當前線程包裝一下,設置模式,形成節點,加入隊列
	private Node addWaiter(Node mode) {
        // 根據mode和thread創建節點
        Node node = new Node(Thread.currentThread(), mode);
        // 記錄一下原尾節點
        Node pred = tail;
        // 尾節點不爲null,隊列不爲空,快速嘗試加入隊尾。
        if (pred != null) {
            // 讓node的prev指向尾節點
            node.prev = pred;
            // CAS操作設置node爲新的尾節點,tail = node
            if (compareAndSetTail(pred, node)) {
                // 設置成功,讓原尾節點的next指向新的node,實現雙向鏈接
                pred.next = node;
                // 入隊成功,返回
                return node;
            }
        }
        // 快速入隊失敗,進行不斷嘗試
        enq(node);
        return node;
    }

幾個注意點:

  • 入隊的操作其實就是將線程通過指定模式包裝爲Node節點,如果隊列尾節點不爲null,利用CAS嘗試快速加入隊尾。
  • 快速入隊失敗的原因有兩個:
    • 隊列爲空,即還沒有進行初始化。
    • CAS設置尾節點的時候失敗。
  • 在第一次快速入隊失敗後,將會走到enq(node)邏輯,不斷進行嘗試,直到設置成功。

不斷嘗試Node enq(final Node node)

    private Node enq(final Node node) {
        // 自旋,俗稱死循環,直到設置成功爲止
        for (;;) {
            // 記錄原尾節點
            Node t = tail;
            // 第一種情況:隊列爲空,原先head和tail都爲null,
            // 通過CAS設置head爲哨兵節點,如果設置成功,tail也指向哨兵節點
            if (t == null) { // Must initialize
                // 初始化head節點
                if (compareAndSetHead(new Node()))
                    // tail指向head,下個線程來的時候,tail就不爲null了,就走到了else分支
                    tail = head;
            // 第二種情況:CAS設置尾節點失敗的情況,和addWaiter一樣,只不過它在for(;;)中
            } else {
                // 入隊,將新節點的prev指向tail
                node.prev = t;
                // CAS設置node爲尾部節點
                if (compareAndSetTail(t, node)) {
                    //原來的tail的next指向node
                    t.next = node;
                    return t;
                }
            }
        }
    }

enq的過程是自選設置隊尾的過程,如果設置成功,就返回。如果設置失敗,則一直嘗試設置,理念就是,我總能等待設置成功那一天。

我們還可以發現,head是延遲初始化的,在第一個節點嘗試入隊的時候,head爲null,這時使用了new Node()創建了一個不代表任何線程的節點,作爲虛擬頭節點,且我們需要注意它的waitStatus初始化爲0,這一點對我們之後分析有指導意義。

如果是CAS失敗導致重複嘗試,那就還是讓他繼續CAS好了。

boolean acquireQueued(Node, int)

    // 這個方法如果返回true,代碼將進入selfInterrupt()
	final boolean acquireQueued(final Node node, int arg) {
        // 注意默認爲true
        boolean failed = true;
        try {
            // 是否中斷
            boolean interrupted = false;
            // 自旋,即死循環
            for (;;) {
                // 得到node的前驅節點
                final Node p = node.predecessor();
                // 我們知道head是虛擬的頭節點,p==head表示如果node爲阻塞隊列的第一個真實節點
                // 就執行tryAcquire邏輯,這裏tryAcquire也需要由子類實現
                if (p == head && tryAcquire(arg)) {
                    // tryAcquire獲取成功走到這,執行setHead出隊操作 
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 走到這有兩種情況 1.node不是第一個節點 2.tryAcquire爭奪鎖失敗了
                // 這裏就判斷 如果當前線程爭鎖失敗,是否需要掛起當前這個線程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 死循環退出,只有tryAcquire獲取鎖失敗的時候failed才爲true
            if (failed)
                cancelAcquire(node);
        }
    }

出隊void setHead(Node)

CLU同步隊列遵循FIFO,首節點的線程釋放同步狀態後,喚醒下一個節點。將隊首節點出隊的操作實際上就是,將head指針指向將要出隊的節點就可以了。

    private void setHead(Node node) {
        // head指針指向node
        head = node;
        // 釋放資源
        node.thread = null;
        node.prev = null;
    }

boolean shouldParkAfterFailedAcquire(Node,Node)

    /**
     * 走到這有兩種情況 1.node不是第一個節點 2.tryAcquire爭奪鎖失敗了
     * 這裏就判斷 如果當前線程爭鎖失敗,是否需要掛起當前這個線程
     *
     * 這裏pred是前驅節點, node就是當前節點
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 前驅節點的waitStatus
        int ws = pred.waitStatus;
        // 前驅節點爲SIGNAL【-1】直接返回true,表示當前節點可以被直接掛起
        if (ws == Node.SIGNAL)
            return true;
        // ws>0 CANCEL 說明前驅節點取消了排隊
        if (ws > 0) {
            // 下面這段循環其實就是跳過所有取消的節點,找到第一個正常的節點
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 將該節點的後繼指向node,建立雙向連接
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             * 官方說明:走到這waitStatus只能是0或propagate,默認情況下,當有新節點入隊時,waitStatus總是爲0
             * 下面用CAS操作將前驅節點的waitStatus值設置爲signal
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 返回false,接着會再進入循環,此時前驅節點爲signal,返回true
        return false;
    }

針對前驅節點的waitStatus有三種情況:

等待狀態不會爲 Node.CONDITION ,因爲它用在 ConditonObject 中

  1. ws==-1,即爲Node.SIGNAL,表示當前節點node可以被直接掛起,在pred線程釋放同步狀態時,會對node線程進行喚醒。
  2. ws > 0,即爲Node.CANCELLED,說明前驅節點已經取消了排隊【可能是超時,可能是被中斷】,則需要找到前面沒有取消的前驅節點,一直找,直到找到爲止。
  3. ws == 0 or ws == Node.PROPAGATE:
    • 默認情況下,當有新節點入隊時,waitStatus總是爲0,用CAS操作將前驅節點的waitStatus值設置爲signal,下一次進來的時候,就走到了第一個分支。
    • 當釋放鎖的時候,會將佔用鎖的節點的ws狀態更新爲0。

PROPAGATE表示共享模式下,前驅節點不僅會喚醒後繼節點,同時也可能會喚醒後繼的後繼。

我們可以發現,這個方法在第一次走進來的時候是不會返回true的。原因在於,返回true的條件時前驅節點的狀態爲SIGNAL,而第一次的時候還沒有給前驅節點設置SIGNAL呢,只有在CAS設置了狀態之後,第二次進來纔會返回true。

那SIGNAL的意義到底是什麼呢?

這裏引用:併發編程——詳解 AQS CLH 鎖 # 爲什麼 AQS 需要一個虛擬 head 節點

waitStatus這裏用ws簡稱,每個節點都有ws變量,用於表示該節點的狀態。初始化的時候爲0,如果被取消爲1,signal爲-1。

如果某個節點的狀態是signal的,那麼在該節點釋放鎖的時候,它需要喚醒下一個節點。

因此,每個節點在休眠之前,如果沒有將前驅節點的ws設置爲signal,那麼它將永遠無法被喚醒。

因此我們會發現上面當前驅節點的ws爲0或propagate的時候,採用cas操作將ws設置爲signal,目的就是讓上一個節點釋放鎖的時候能夠通知自己。

boolean parkAndCheckInterrupt()

    private final boolean parkAndCheckInterrupt() {
        // 掛起當前線程
        LockSupport.park(this);
        return Thread.interrupted();
    }

shouldParkAfterFailedAcquire方法返回true之後,就會調用該方法,掛起當前線程。

LockSupport.park(this)方法掛起的線程有兩種途徑被喚醒:1.被unpark() 2.被interrupt()。

需要注意這裏的Thread.interrupted()會清除中斷標記位。

void cancelAcquire(node)

上面tryAcquire獲取鎖失敗的時候,會走到這個方法。

    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
		// 將節點的線程置空
        node.thread = null;

        // 跳過所有的取消的節點
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        // 這裏在沒有併發的情況下,preNext和node是一致的
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here. 可以直接寫而不是用CAS
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        // 設置node節點爲取消狀態
        node.waitStatus = Node.CANCELLED;

        // 如果node爲尾節點就CAS將pred設置爲新尾節點
        if (node == tail && compareAndSetTail(node, pred)) {
            // 設置成功之後,CAS將pred的下一個節點置爲空
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head && // pred不是首節點
                ((ws = pred.waitStatus) == Node.SIGNAL || // pred的ws爲SIGNAL 或 可以被CAS設置爲SIGNAL
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) { // pred線程非空
                // 保存node 的下一個節點
                Node next = node.next; 
                // node的下一個節點不是cancelled,就cas設置pred的下一個節點爲next
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 上面的情況除外,則走到這個分支,喚醒node的下一個可喚醒節點線程
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

釋放資源

boolean release(int arg)

    public final boolean release(int arg) {
        if (tryRelease(arg)) { // 子類實現tryRelease方法
            // 獲得當前head
            Node h = head;
            // head不爲null並且head的等待狀態不爲0
            if (h != null && h.waitStatus != 0)
                // 喚醒下一個可以被喚醒的線程,不一定是next哦
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
  • tryRelease(int)是AQS提供給子類實現的鉤子方法,子類可以自定義實現獨佔式釋放資源的方式,釋放成功並返回true,否則返回false。
  • unparkSuccessor(node)方法用於喚醒等待隊列中下一個可以被喚醒的線程,不一定是下一個節點next,比如它可能是取消狀態。
  • head 的ws必須不等於0,爲什麼呢?當一個節點嘗試掛起自己之前,都會將前置節點設置成SIGNAL -1,就算是第一個加入隊列的節點,在獲取鎖失敗後,也會將虛擬節點設置的 ws 設置成 SIGNAL,而這個判斷也是防止多線程重複釋放,接下來我們也能看到釋放的時候,將ws設置爲0的操作。

void unparkSuccessor(Node node)

    
	private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        // 如果node的waitStatus<0爲signal,CAS修改爲0
        // 將 head 節點的 ws 改成 0,清除信號。表示,他已經釋放過了。不能重複釋放。
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        // 喚醒後繼節點,但是有可能後繼節點取消了等待 即 waitStatus == 1
        Node s = node.next;
        // 如果後繼節點爲空或者它已經放棄鎖了
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 從隊尾往前找,找到沒有沒取消的所有節點排在最前面的【直到t爲null或t==node才退出循環嘛】
            for (Node t = tail; t != null && t != node; t = t.prev)
                // 如果>0表示節點被取消了,就一直向前找唄,找到之後不會return,還會一直向前
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 如果後繼節點存在且沒有被取消,會走到這,直接喚醒後繼節點即可
        if (s != null)
            LockSupport.unpark(s.thread);
    }

參考閱讀

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