AQS簡單源碼說明

AbstractQueuedSynchronizer

所謂AQS,指的是AbstractQueuedSynchronizer,它提供了一種實現阻塞鎖和一系列依賴FIFO等待隊列的同步器的框架,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等併發類均是基於AQS來實現的,具體用法是通過繼承AQS實現其模板方法,然後將子類作爲同步組件的內部類。

變量waitStatus則表示當前Node結點的等待狀態,共有5種取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示當前結點已取消調度。當timeout或被中斷(響應中斷的情況下),會觸發變更爲此狀態,進入該狀態後的結點將不會再變化。
  • SIGNAL(-1):表示後繼結點在等待當前結點喚醒。後繼結點入隊時,會將前繼結點的狀態更新爲SIGNAL。
  • CONDITION(-2):表示結點等待在Condition上,當其他線程調用了Condition的signal()方法後,CONDITION狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。
  • PROPAGATE(-3):共享模式下,前繼結點不僅會喚醒其後繼結點,同時也可能會喚醒後繼的後繼結點。
  • 0:新結點入隊時的默認狀態。

AQS內部使用CLH算法維護等待隊列,CLH鎖即Craig, Landin, and Hagersten (CLH) locks。CLH鎖是一個自旋鎖。能確保無飢餓性。提供先來先服務的公平性。

CLH鎖也是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程僅僅在本地變量上自旋,它不斷輪詢前驅的狀態,假設發現前驅釋放了鎖就結束自旋。

CLH隊列中的結點QNode中含有一個locked字段,該字段若爲true表示該線程須要獲取鎖,且不釋放鎖。爲false表示線程釋放了鎖。

結點之間是通過隱形的鏈表相連,之所以叫隱形的鏈表是由於這些結點之間沒有明顯的next指針,而是通過myPred所指向的結點的變化情況來影響myNode的行爲。

CLHLock上另一個尾指針,始終指向隊列的最後一個結點。

CLHLock的類圖例如以下所看到的:

在這裏插入圖片描述

當一個線程須要獲取鎖時,會創建一個新的QNode。將當中的locked設置爲true表示須要獲取鎖。然後線程對tail域調用getAndSet方法,使自己成爲隊列的尾部。同一時候獲取一個指向其前趨的引用myPred,然後該線程就在前趨結點的locked字段上旋轉。直到前趨結點釋放鎖。

當一個線程須要釋放鎖時,將當前結點的locked域設置爲false。同一時候回收前趨結點。例如以下圖所看到的,線程A須要獲取鎖。其myNode域爲true。些時tail指向線程A的結點,然後線程B也增加到線程A後面。tail指向線程B的結點。然後線程A和B都在它的myPred域上旋轉,一量它的myPred結點的locked字段變爲false,它就能夠獲取鎖掃行。明顯線程A的myPred locked域爲false,此時線程A獲取到了鎖。

在這裏插入圖片描述

獲取獨佔鎖:

AQS實現類中類似於ReentrantLock類,在獲取鎖的操作中最後調用的是如下方法。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
 }

tryAcquire方法由具體實現類實現,顧名思義用來獲取資源,在第一次獲取資源失敗後進入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。

首先看下addWaiter(Node.EXCLUSIVE):

private Node addWaiter(Node mode) {
    //創建獨佔模式的節點
    Node node = new Node(mode);
	//自旋插入節點
    for (;;) {
        //獲取尾部節點
        Node oldTail = tail;
        //如果尾部節點不爲空
        if (oldTail != null) {
            //新節點的前置節點設置爲原先的尾節點
            node.setPrevRelaxed(oldTail);
            //將新節點設置爲尾節點
            if (compareAndSetTail(oldTail, node)) {
                //原先尾節點next節點設置爲新節點
                oldTail.next = node;
                return node;
            }
        } else {
            //如果沒有尾節點則說明隊列爲空需要初始化
            initializeSyncQueue();
        }
    }
}

如上爲在等待隊列中插入當前獲取資源的節點。

接下來看acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        //自旋
        for (;;) {
            //獲取當前節點的前置節點
            final Node p = node.predecessor();
            //如果前置節點爲頭節點,說明馬上就輪到自己了,可以先嚐試獲取資源
            //在頭結點釋放資源後並喚醒下一個節點的時候p==head成立,進入循環
            //這裏保證FIFO
            if (p == head && tryAcquire(arg)) {
                //如果獲取成功,將當前節點設置爲頭節點
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            //保證前置節點狀態爲SIGNAL
            if (shouldParkAfterFailedAcquire(p, node))
                //調用park方法將自己阻塞,激活後檢查中斷狀態並與interrupted或運算
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}

再看下shouldParkAfterFailedAcquire(p, node)方法:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //獲取前繼節點的waitStatus
    int ws = pred.waitStatus;
    //如果ws爲SIGNAL狀態,表面前繼節點釋放資源或中斷後會喚醒自己直接返回true
    if (ws == Node.SIGNAL)
        return true;
    //如果ws>0即爲取消狀態,跳過此前置節點,一直往前找,直到找到waitStatus<0的節點,並將此節點設置爲	 //node的前置節點,同時設置此節點的後置節點爲node
    if (ws > 0) {
        do {
            //這裏往前變量才能保證if (p == head && tryAcquire(arg)) 跳出循環
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 將前繼節點的ws值設置爲Node.SIGNAL,以保證下次自旋時,shouldParkAfterFailedAcquire直接返回true
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

釋放獨佔鎖:

AQS實現類中類似於ReentrantLock類,在釋放鎖的操作中最後調用的是如下方法。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        //獲取頭節點
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease方法由具體實現類實現,顧名思義用來釋放資源,釋放成功進入後面代碼。

接下來看unparkSuccessor方法:

private void unparkSuccessor(Node node) {
    //獲取頭結點waitStatus
    int ws = node.waitStatus;
    if (ws < 0)
        //如果ws小於0,將其waitStatus置爲0
        node.compareAndSetWaitStatus(ws, 0);
	
    Node s = node.next;
    //如果頭結點的nest節點爲空或者waitStatus大於0,則從尾節點從後往前查找
    //搜索到等待隊列中最靠前的ws值非正且非null的節點
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node p = tail; p != node && p != null; p = p.prev)
            if (p.waitStatus <= 0)
                s = p;
    }
    //喚醒下一個節點線程
    if (s != null)
        LockSupport.unpark(s.thread);
}

後繼節點的阻塞線程被喚醒後,就進入到acquireQueued()的if (p == head && tryAcquire(arg))的判斷中,此時被喚醒的線程將嘗試獲取資源。

當然,如果被喚醒的線程所在節點的前繼節點不是頭結點,經過shouldParkAfterFailedAcquire的調整,也會移動到等待隊列的前面,直到其前繼節點爲頭結點。

獲取共享鎖

AQS實現類中類似於ReentrantReadWriteLock類,在獲取共享鎖的操作中最後調用的是如下方法。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

tryAcquireShared方法爲AQS集成類實現,獲取失敗後進入doAcquireShared方法:

private void doAcquireShared(int arg) {
    //此addWaiter與獲取獨佔鎖中的一致,將一個共享節點放入等待隊列隊尾
    final Node node = addWaiter(Node.SHARED);
    boolean interrupted = false;
    try {
        //自旋操作
        for (;;) {
            //獲取新節點前驅節點
            final Node p = node.predecessor();
            if (p == head) {
                //如果p就是頭結點進入下面邏輯
                //嘗試獲取資源
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //獲取成功後將當前節點設爲頭結點,如果r>0並喚醒後繼節點
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    return;
                }
            }
            //保證前置節點狀態爲SIGNAL,方法詳情分析見獨佔鎖中分析
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    } finally {
        if (interrupted)
            selfInterrupt();
    }
}

上述代碼中與獲取獨佔鎖邏輯大致一樣,不同的地方在於setHeadAndPropagate方法,跟蹤代碼如下:

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    //將當前節點設爲頭節點
    setHead(node);
    //當前節點獲取成功後返回值大於0,表面後繼節點有可能獲取資源成功,所以需要喚醒後繼節點
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

喚醒後繼節點的方法爲doReleaseShared()代碼如下:

private void doReleaseShared() {
    //自旋進行喚醒,由於多個線程在操作故每次獲取的頭結點可能會變,操作完頭結點沒變則跳出循環
    for (;;) {
        //獲取頭結點
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //ws爲SIGNAL則將狀態置爲0
            if (ws == Node.SIGNAL) {
                if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                //喚醒後繼節點
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

釋放共享鎖:

AQS實現類中類似於ReentrantReadWriteLock類,在釋放共享鎖的操作中最後調用的是如下方法。

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

tryReleaseShared由實現類實現,doReleaseShared上面已經分析過了。

至此,共享模式下的資源獲取/釋放就講解完了,下面以一個具體場景來概括一下:

整個獲取/釋放資源的過程是通過傳播完成的,如最開始有10個資源,線程A、B、C分別需要5、4、3個資源。

  • A線程獲取到5個資源,其發現資源還剩餘5個,則喚醒B線程;
  • B線程獲取到4個資源,其發現資源還剩餘1個,喚醒C線程;
  • C線程嘗試取3個資源,但發現只有1個資源,繼續阻塞;
  • A線程釋放1個資源,其發現資源還剩餘2個,故喚醒C線程;
  • C線程嘗試取3個資源,但發現只有2個資源,繼續阻塞;
  • B線程釋放2個資源,其發現資源還剩餘4個,喚醒C線程;
  • C線程獲取3個資源,其發現資源還剩1個,繼續喚醒後續等待的D線程;

參考文章:https://www.jianshu.com/p/0f876ead2846
https://blog.csdn.net/varyall/article/details/80317488

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