91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

java同步器AQS架構如何釋放鎖和同步隊列

發布時間:2022-03-14 09:19:56 來源:億速云 閱讀:230 作者:小新 欄目:開發技術

這篇文章主要為大家展示了“java同步器AQS架構如何釋放鎖和同步隊列”,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“java同步器AQS架構如何釋放鎖和同步隊列”這篇文章吧。

    引導語

    AQS 的內容太多,所以我們分成了兩個章節,沒有看過 AQS 上半章節的同學可以回首看一下哈,上半章節里面說了很多鎖的基本概念,基本屬性,如何獲得鎖等等,本章我們主要聊下如何釋放鎖和同步隊列兩大部分。

    1、釋放鎖

    釋放鎖的觸發時機就是我們常用的 Lock.unLock () 方法,目的就是讓線程釋放對資源的訪問權(流程見整體架構圖紫色路線)。

    釋放鎖也是分為兩類,一類是排它鎖的釋放,一類是共享鎖的釋放,我們分別來看下。

    1.1、釋放排它鎖 release

    排它鎖的釋放就比較簡單了,從隊頭開始,找它的下一個節點,如果下一個節點是空的,就會從尾開始,一直找到狀態不是取消的節點,然后釋放該節點,源碼如下:

    // unlock 的基礎方法
    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;
    }
    // 很有意思的方法,當線程釋放鎖成功后,從 node 開始喚醒同步隊列中的節點
    // 通過喚醒機制,保證線程不會一直在同步隊列中阻塞等待
    private void unparkSuccessor(Node node) {
        // node 節點是當前釋放鎖的節點,也是同步隊列的頭節點
        int ws = node.waitStatus;
        // 如果節點已經被取消了,把節點的狀態置為初始化
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 拿出 node 節點的后面一個節點
        Node s = node.next;
        // s 為空,表示 node 的后一個節點為空
        // s.waitStatus 大于0,代表 s 節點已經被取消了
        // 遇到以上這兩種情況,就從隊尾開始,向前遍歷,找到第一個 waitStatus 字段不是被取消的
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 這里從尾迭代,而不是從頭開始迭代是有原因的。
            // 主要是因為節點被阻塞的時候,是在 acquireQueued 方法里面被阻塞的,喚醒時也一定會在 acquireQueued 方法里面被喚醒,喚醒之后的條件是,判斷當前節點的前置節點是否是頭節點,這里是判斷當前節點的前置節點,所以這里必須使用從尾到頭的迭代順序才行,目的就是為了過濾掉無效的前置節點,不然節點被喚醒時,發現其前置節點還是無效節點,就又會陷入阻塞。
            for (Node t = tail; t != null && t != node; t = t.prev)
                // t.waitStatus <= 0 說明 t 沒有被取消,肯定還在等待被喚醒
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 喚醒以上代碼找到的線程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

    1.2、釋放共享鎖 releaseShared

    釋放共享鎖的方法是 releaseShared,主要分成兩步:

    tryReleaseShared 嘗試釋放當前共享鎖,失敗返回 false,成功走 2;

    喚醒當前節點的后續阻塞節點,這個方法我們之前看過了,線程在獲得共享鎖的時候,就會去喚醒其后面的節點,方法名稱為:doReleaseShared。

    我們一起來看下 releaseShared 的源碼:

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

    2、條件隊列的重要方法

    在看條件隊列的方法之前,我們先得弄明白為什么有了同步隊列,還需要條件隊列?

    主要是因為并不是所有場景一個同步隊列就可以搞定的,在遇到鎖 + 隊列結合的場景時,就需要 Lock + Condition 配合才行,先使用 Lock 來決定哪些線程可以獲得鎖,哪些線程需要到同步隊列里面排隊阻塞;獲得鎖的多個線程在碰到隊列滿或者空的時候,可以使用 Condition 來管理這些線程,讓這些線程阻塞等待,然后在合適的時機后,被正常喚醒。

    同步隊列 + 條件隊列聯手使用的場景,最多被使用到鎖 + 隊列的場景中。

    所以說條件隊列也是不可或缺的一環。

    接下來我們來看一下條件隊列一些比較重要的方法,以下方法都在 ConditionObject 內部類中。

    2.1、入隊列等待 await

    // 線程入條件隊列
    public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 加入到條件隊列的隊尾
        Node node = addConditionWaiter();
        // 標記位置 A
        // 加入條件隊列后,會釋放 lock 時申請的資源,喚醒同步隊列隊列頭的節點
        // 自己馬上就要阻塞了,必須馬上釋放之前 lock 的資源,不然自己不被喚醒的話,別的線程永遠得不到該共享資源了
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        // 確認node不在同步隊列上,再阻塞,如果 node 在同步隊列上,是不能夠上鎖的
        // 目前想到的只有兩種可能:
        // 1:node 剛被加入到條件隊列中,立馬就被其他線程 signal 轉移到同步隊列中去了
        // 2:線程之前在條件隊列中沉睡,被喚醒后加入到同步隊列中去
        while (!isOnSyncQueue(node)) {
            // this = AbstractQueuedSynchronizer$ConditionObject
            // 阻塞在條件隊列上
            LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        // 標記位置 B
        // 其他線程通過 signal 已經把 node 從條件隊列中轉移到同步隊列中的數據結構中去了
        // 所以這里節點蘇醒了,直接嘗試 acquireQueued
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            // 如果狀態不是CONDITION,就會自動刪除
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }

    await 方法有幾點需要特別注意:

    上述代碼標記位置 A 處,節點在準備進入條件隊列之前,一定會先釋放當前持有的鎖,不然自己進去條件隊列了,其余的線程都無法獲得鎖了;上述代碼標記位置 B 處,此時節點是被 Condition.signal 或者 signalAll 方法喚醒的,此時節點已經成功的被轉移到同步隊列中去了(整體架構圖中藍色流程),所以可以直接執行 acquireQueued 方法;Node 在條件隊列中的命名,源碼喜歡用 Waiter 來命名,所以我們在條件隊列中看到 Waiter,其實就是 Node。

    await 方法中有兩個重要方法:addConditionWaiter 和 unlinkCancelledWaiters,我們一一看下。

    2.1.1、addConditionWaiter

    addConditionWaiter 方法主要是把節點放到條件隊列中,方法源碼如下:

    // 增加新的 waiter 到隊列中,返回新添加的 waiter
    // 如果尾節點狀態不是 CONDITION 狀態,刪除條件隊列中所有狀態不是 CONDITION 的節點
    // 如果隊列為空,新增節點作為隊列頭節點,否則追加到尾節點上
    private Node addConditionWaiter() {
        Node t = lastWaiter;
        // If lastWaiter is cancelled, clean out.
        // 如果尾部的 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;
    }

    整體過程比較簡單,就是追加到隊列的尾部,其中有個重要方法叫做 unlinkCancelledWaiters,這個方法會刪除掉條件隊列中狀態不是 CONDITION 的所有節點,我們來看下 unlinkCancelledWaiters 方法的源碼,如下:

    2.1.2、unlinkCancelledWaiters
    // 會檢查尾部的 waiter 是不是已經不是CONDITION狀態了
    // 如果不是,刪除這些 waiter
    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;
        }
    }

    為了方便大家理解這個方法,畫了一個釋義圖,如下:

    java同步器AQS架構如何釋放鎖和同步隊列

    2.2、單個喚醒 signal 

    signal 方法是喚醒的意思,比如之前隊列滿了,有了一些線程因為 take 操作而被阻塞進條件隊列中,突然隊列中的元素被線程 A 消費了,線程 A 就會調用 signal 方法,喚醒之前阻塞的線程,會從條件隊列的頭節點開始喚醒(流程見整體架構圖中藍色部分),源碼如下:

    // 喚醒阻塞在條件隊列中的節點
    public final void signal() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        // 從頭節點開始喚醒
        Node first = firstWaiter;
        if (first != null)
            // doSignal 方法會把條件隊列中的節點轉移到同步隊列中去
            doSignal(first);
    }
    // 把條件隊列頭節點轉移到同步隊列去
    private void doSignal(Node first) {
        do {
            // nextWaiter為空,說明到隊尾了
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            // 從隊列頭部開始喚醒,所以直接把頭節點.next 置為 null,這種操作其實就是把 node 從條件隊列中移除了
            // 這里有個重要的點是,每次喚醒都是從隊列頭部開始喚醒,所以把 next 置為 null 沒有關系,如果喚醒是從任意節點開始喚醒的話,就會有問題,容易造成鏈表的割裂
            first.nextWaiter = null;
            // transferForSignal 方法會把節點轉移到同步隊列中去
            // 通過 while 保證 transferForSignal 能成功
            // 等待隊列的 node 不用管他,在 await 的時候,會自動清除狀態不是 Condition 的節點(通過 unlinkCancelledWaiters 方法)
            // (first = firstWaiter) != null  = true 的話,表示還可以繼續循環, = false 說明隊列中的元素已經循環完了
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }

    我們來看下最關鍵的方法:transferForSignal。

    // 返回 true 表示轉移成功, false 失敗
    // 大概思路:
    // 1. node 追加到同步隊列的隊尾
    // 2. 將 node 的前一個節點狀態置為 SIGNAL,成功直接返回,失敗直接喚醒
    // 可以看出來 node 的狀態此時是 0 了
    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        // 將 node 的狀態從 CONDITION 修改成初始化,失敗返回 false
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        // 當前隊列加入到同步隊列,返回的 p 是 node 在同步隊列中的前一個節點
        // 看命名是 p,實際是 pre 單詞的縮寫
        Node p = enq(node);
        int ws = p.waitStatus;
        // 狀態修改成 SIGNAL,如果成功直接返回
        // 把當前節點的前一個節點修改成 SIGNAL 的原因,是因為 SIGNAL 本身就表示當前節點后面的節點都是需要被喚醒的
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            // 如果 p 節點被取消,或者狀態不能修改成SIGNAL,直接喚醒
            LockSupport.unpark(node.thread);
        return true;
    }

    整個源碼下來,我們可以看到,喚醒條件隊列中的節點,實際上就是把條件隊列中的節點轉移到同步隊列中,并把其前置節點狀態置為 SIGNAL。

    2.3、全部喚醒 signalAll

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

        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 拿到頭節點
            Node first = firstWaiter;
            if (first != null)
                // 從頭節點開始喚醒條件隊列中所有的節點
                doSignalAll(first);
        }
        // 把條件隊列所有節點依次轉移到同步隊列去
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                // 拿出條件隊列隊列頭節點的下一個節點
                Node next = first.nextWaiter;
                // 把頭節點從條件隊列中刪除
                first.nextWaiter = null;
                // 頭節點轉移到同步隊列中去
                transferForSignal(first);
                // 開始循環頭節點的下一個節點
                first = next;
            } while (first != null);
        }

    從源碼中可以看出,其本質就是 for 循環調用 transferForSignal 方法,將條件隊列中的節點循環轉移到同步隊列中去。

    3、總結

    AQS 源碼終于說完了,你都懂了么,可以在默默回憶一下 AQS 架構圖,看看這張圖現在能不能看懂了。

    java同步器AQS架構如何釋放鎖和同步隊列

    以上是“java同步器AQS架構如何釋放鎖和同步隊列”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    喀什市| 静宁县| 平邑县| 申扎县| 神池县| 页游| 临颍县| 汤原县| 皮山县| 商水县| 丰原市| 泸溪县| 盐边县| 澄城县| 曲阳县| 隆回县| 安阳县| 绥德县| 镇安县| 西充县| 璧山县| 黑山县| 榆社县| 定西市| 罗定市| 通山县| 崇明县| 城固县| 定南县| 永清县| 丽江市| 开鲁县| 普定县| 沭阳县| 靖远县| 宁明县| 宜川县| 株洲市| 南丹县| 拉萨市| 西畴县|