AQS底層原理分析講解


AQS 是什么
在 Lock 中,用到了一個同步隊列 AQS,全稱 AbstractQueuedSynchronizer,它是一個同步工具也是 Lock 用來實現線程同步的核心組件。如果你搞懂了 AQS,那么 J.U.C 中絕大部分的工具都能輕松掌握。
AQS 的兩種功能
從使用層面來說,AQS 的功能分為兩種: 獨占和共享
獨占鎖:每次只能有一個線程持有鎖,比如前面給大家演示的 ReentrantLock 就是以獨占方式實現的互斥鎖。
共 享 鎖 : 允許多個線程同時獲取鎖 , 並發訪問共享資源 , 比如ReentrantReadWriteLock。
AQS 的內部實現
AQS 隊列內部維護的是一個 FIFO 的雙向鏈表,這種結構的特點是每個數據結構都有兩個指針,分別指向直接的后繼節點和直接前驅節點。所以雙向鏈表可以從任意一個節點開始很方便的訪問前驅和后繼。每個 Node 其實是由線程封裝,當線程爭搶鎖失敗后會封裝成 Node 加入到 ASQ 隊列中去;當獲取鎖的線程釋放鎖以后,會從隊列中喚醒一個阻塞的節點(線程)。

AQS核心變量

1、volatile int state

AQS使用一個 volatile 修飾的 int 變量來表示同步狀態,當 state>0 時,表示已經獲取到了鎖,當 state=0 時,表示釋放了鎖。
它提供了三個方法:
getState()
seState(int newState)
compareAndSetState(int expect, int update)
這三個方法用於對同步狀態state進行操作,當然,AQS可以確保對state操作的安全性。

2、FIFO同步隊列
AQS通過內置的FIFO同步隊列,來完成資源獲取線程的排隊工作。
如果當前線程獲取同步狀態失敗時,AQS會將當前線程以及等待狀態等信息,構造成一個節點(Node)並將其加入同步隊列
同時,會阻塞當前線程,當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。

這個所謂的FIFO就是CLH隊列:

CLHNode的組成:

釋放鎖以及添加線程對於隊列的變化(通俗的講就是入隊和出隊

當出現鎖競爭以及釋放鎖的時候,AQS 同步隊列中的節點會發生變化,首先看一下添加節點的場景。

入隊源碼:

  /**
     * Creates and enqueues node for current thread and given mode.
     * 新節點的創建並入隊在制定模式下(共享式和獨占式)
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
      private Node addWaiter(Node mode) {
        //1、為當前線程創建新節點
        Node node = new Node(Thread.currentThread(), mode);
        // 嘗試快速插入,如果失敗則用enq插入

        //1、獲取同步器中的tail指向的節點,即:未插入新節點時的尾節點(暫且稱之為原隊列中的尾節點)
        Node pred = tail;
        if (pred != null) {//判斷尾節點不為空,為空則使用enq插入,enq中會存在創建head和Tail節點的邏輯
            //2、新節點的前驅節點指向原尾節點
            node.prev = pred;
            //3、使用CAS設置尾節點(AQS代碼風格之一:將操作放入if判斷中)
            if (compareAndSetTail(pred, node)) {
                //4、將原尾節點的next指向新節點
                pred.next = node;
                return node;
            }
        }
        //如果快速插入隊列失敗,則用enq進行插入
        enq(node);
        return node;
    }


  /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * 將節點插入隊列,如果有必要(未節點為空),即:隊列為空,則初始化隊列
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        //類似節點獲取同步狀態時的自旋,其實就是有返回條件的死循環
        for (;;) {
            //1、將同步器中的未節點賦給臨時變量t
            Node t = tail;
            if (t == null) { // Must initialize
                //2、如果未節點為空,就是隊列為空,則說明隊列為空,新建一個節點,使用CAS設置頭結點。,CAS保證操作的原子性
                if (compareAndSetHead(new Node()))
                    //3、如果頭結點設置成功,則將同步器中的未節點指向頭結點,然后繼續步驟1
                    tail = head;
            } else {
                //4、將新節點的前驅節點指向原隊列中的未節點
                node.prev = t;
                //5、使用CAS設置未節點,即:同步器中的tail指向新節點
                if (compareAndSetTail(t, node)) {
                    //6、如果未節點設置成功,則將原未節點的next指向新節點
                    t.next = node;
                    return t;
                }
            }
        }
    }

 這里會涉及到兩個變化

1. 新的線程封裝成 Node 節點追加到同步隊列中,設置 prev 節點以及修改當前節點的前置節點的 next 節點指向自己。
2. 通過 CAS 將tail 重新指向新的尾部節點head 節點表示獲取鎖成功的節點,當頭結點在釋放同步狀態時,會喚醒后繼節點,如果后繼節點獲得鎖成功,會把自己設置為頭結點,節點的變化過程如下:

這個過程也是涉及到兩個變化

1. 修改 head 節點指向下一個獲得鎖的節點
2. 新的獲得鎖的節點,將 prev 的指針指向 null設置 head 節點不需要用 CAS,原因是設置 head 節點是由獲得鎖的線程來完成的,而同步鎖只能由一個線程獲得,所以不需要 CAS 保證,只需要把 head 節點設置為原首節點的后繼節點,並且斷開原 head 節點的 next 引用即可。
AQS中最重要的就是aquire(int arg) 方法,它是AQS中提供的模板方法,該方法為獨占式獲取同步狀態,會忽略中斷,也就是說,由於線程獲取同步狀態失敗加入到CLH同步隊列中,后續對線程進行中斷操作時,線程不會從同步隊列中移除。
acquire方法流程圖如下:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM