1. 程式人生 > 其它 >mysql面試指南-mysql版本類問題

mysql面試指南-mysql版本類問題

AQS

<前一整子做了一個AQS的技術分享,特將內容整理記錄如下,_>

什麼是AQS?

AQS 的全稱是 AbstractQueuedSynchronizer,即抽象佇列同步器。是Java併發工具的基礎,採用樂觀鎖,通過CAS與自旋輕量級的獲取鎖。維護了一個volatile int state(代表共享資源)和一個FIFO執行緒等待佇列(多執行緒爭用資源被阻塞時會進入此佇列)。很多JUC包,比如ReentrantLock、Semaphore、CountDownLatch等併發類均是繼承AQS,通過AQS的模板方法,來實現的。

原理

AQS的組成結構

AQS = 同步狀態(volatile int state)

+ 同步佇列(即等待佇列,FIFO的CLH佇列) + 條件佇列(ConditionObject)

  • state:代表共享資源。volatile 保證併發讀,CAS 保證併發寫
  • 同步佇列(即等待佇列,CLH佇列):是CLH變體的虛擬雙向佇列(先進先出FIFO)來等待獲取共享資源。當前執行緒可以通過signal和signalAll將條件佇列中的節點轉移到同步佇列中
  • 條件佇列(ConditionObject):當前執行緒存在於同步佇列的頭節點,可以通過await從同步佇列轉移到條件佇列中

同步狀態

在AQS中維護了一個同步狀態變數state,getState函式獲取同步狀態,setState、compareAndSetState函式修改同步狀態,對於AQS來說,執行緒同步的關鍵是對state的操作,可以說獲取、釋放資源是否成功都是由state決定的,比如state>0代表可獲取資源,否則無法獲取,所以state的具體語義由實現者去定義,現有的ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch定義的state語義都不一樣。

  • ReentrantLock的state用來表示是否有鎖資源
  • ReentrantReadWriteLock的state高16位代表讀鎖狀態,低16位代表寫鎖狀態
  • Semaphore的state用來表示可用訊號的個數
  • CountDownLatch的state用來表示計數器的值

CLH佇列

CLH是AQS內部維護的FIFO(先進先出)雙端雙向佇列(方便尾部節點插入),基於連結串列資料結構,當一個執行緒競爭資源失敗,就會將等待資源的執行緒封裝成一個Node節點,通過CAS原子操作插入佇列尾部,最終不同的Node節點連線組成了一個CLH佇列,所以說AQS通過CLH佇列管理競爭資源的執行緒,CLH佇列具有如下幾個優點:

  • 先進先出保證了公平性
  • 非阻塞的佇列,通過自旋鎖和CAS保證節點插入和移除的原子性,實現無鎖快速插入
  • 採用了自旋鎖思想,所以CLH也是一種基於連結串列的可擴充套件、高效能、公平的自旋鎖

Node內部類

NodeAQS的內部類,每個等待資源的執行緒都會封裝成Node節點組成CLH佇列、等待佇列,所以說Node是非常重要的部分,理解它是理解AQS的第一步。

waitStatus

nextWaiter

  • NodeCLH佇列時,nextWaiter表示共享式或獨佔式標記 SHARED/EXCLUSIVE
  • Node在條件佇列時,nextWaiter表示下個Node節點指標

條件佇列

Object的wait、notify函式是配合Synchronized鎖實現執行緒間同步協作的功能,A Q S的ConditionObject條件變數也提供這樣的功能,通過ConditionObject的await和signal兩類函式完成。

ConditionObject內部維護著一個單向條件佇列,不同於CLH佇列,條件佇列只入隊執行await的執行緒節點,並且加入條件佇列的節點,不能在CLH佇列, 條件隊列出隊的節點,會入隊到CLH佇列。

當某個執行緒執行了ConditionObject的await函式,阻塞當前執行緒,執行緒會被封裝成Node節點新增到條件佇列的末端,其他執行緒執行ConditionObject的signal函式,會將條件佇列頭部執行緒節點轉移到C H L佇列參與競爭資源,具體流程如下圖:

流程說明

執行緒獲取資源失敗,封裝成Node節點從CLH佇列尾部入隊並阻塞執行緒,某執行緒釋放資源時會把CLH佇列首部Node節點關聯的執行緒喚醒(此處的首部是指第二個節點,後面會細說),再次獲取資源。

入隊

獲取資源失敗的執行緒需要封裝成Node節點,接著尾部入隊,在AQS中提供addWaiter函式完成Node節點的建立與入隊。

/**
  * @description:  Node節點入隊-CLH佇列
  * @param mode 標記Node.EXCLUSIVE獨佔式 or Node.SHARED共享式
  */
private Node addWaiter(Node mode) {
        // 根據當前執行緒建立節點,等待狀態為0
        Node node = new Node(Thread.currentThread(), mode);
        // 獲取尾節點
        Node pred = tail;
        if (pred != null) {
            // 如果尾節點不等於null,把當前節點的前驅節點指向尾節點
            node.prev = pred;
            // 通過CAS把尾節點指向當前節點
            if (compareAndSetTail(pred, node)) {
                // 之前尾節點的下個節點指向當前節點
                pred.next = node;
                return node;
            }
        }

        // 如果新增失敗或佇列不存在,執行end函式
        enq(node);
        return node;
}

新增節點的時候,如果從CLH佇列已經存在,通過CAS快速將當前節點新增到佇列尾部,如果新增失敗或佇列不存在,則指向enq函式自旋入隊。

/**
  * @description: 自旋cas入隊
  * @param node 節點
  */
private Node enq(final Node node) {
        for (;;) { //迴圈
            //獲取尾節點
            Node t = tail;
            if (t == null) {
                //如果尾節點為空,建立哨兵節點,通過cas把頭節點指向哨兵節點
                if (compareAndSetHead(new Node()))
                    //cas成功,尾節點指向哨兵節點
                    tail = head;
            } else {
                //當前節點的前驅節點設指向之前尾節點
                node.prev = t;
                //cas設定把尾節點指向當前節點
                if (compareAndSetTail(t, node)) {
                    //cas成功,之前尾節點的下個節點指向當前節點
                    t.next = node;
                    return t;
                }
            }
        }
}

通過自旋CAS嘗試往佇列尾部插入節點,直到成功,自旋過程如果發現CLH佇列不存在時會初始化CLH佇列,入隊過程流程如下圖:

第一次迴圈

  • 剛開始C L H佇列不存在,head與tail都指向null
  • 要初始化C L H佇列,會建立一個哨兵節點,head與tail都指向哨兵節點

第二次迴圈

  • 當前執行緒節點的前驅節點指向尾部節點(哨兵節點)
  • 設定當前執行緒節點為尾部,tail指向當前執行緒節點
  • 前尾部節點的後驅節點指向當前執行緒節點(當前尾部節點)

最後結合addWaiter與enq函式,整體看一下入隊流程圖:

出隊

CLH佇列中的節點都是獲取資源失敗的執行緒節點,當持有資源的執行緒釋放資源時,會將head.next指向的執行緒節點喚醒(CLH佇列的第二個節點),如果喚醒的執行緒節點獲取資源成功,執行緒節點清空資訊設定為頭部節點(新哨兵節點),原頭部節點出隊(原哨兵節點acquireQueued函式中的部分程式碼

//1.獲取前驅節點
final Node p = node.predecessor();
//如果前驅節點是首節點,獲取資源(子類實現)
if (p == head && tryAcquire(arg)) {
    //2.獲取資源成功,設定當前節點為頭節點,清空當前節點的資訊,把當前節點變成哨兵節點
    setHead(node);
    //3.原來首節點下個節點指向為null
    p.next = null; // help GC
    //4.非異常狀態,防止指向finally邏輯
    failed = false;
    //5.返回執行緒中斷狀態
    return interrupted;
}

private void setHead(Node node) {
    //節點設定為頭部
    head = node;
    //清空執行緒
    node.thread = null;
    //清空前驅節點
    node.prev = null;
}

只需要關注1~3步驟即可,過程非常簡單,假設獲取資源成功,更換頭部節點,並把頭部節點的資訊清除變成哨兵節點,注意這個過程是不需要使用CAS來保證,因為只有一個執行緒能夠成功獲取到資源。