AQS(AbstractQueuedSynchronizer)的實現原理

前言

前一篇文章講了一下AQS是什麼以及AQS可重寫的方法、提供的模板方法,本篇就從以下幾點來寫一下同步器的實現原理。

  • 同步隊列
  • 獨佔式同步狀態獲取與釋放
  • 共享式式同步狀態獲取與釋放
  • 超時獲取同步狀態

同步隊列實現原理

同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構建爲一個節點(Node)並將其加入同步隊列,同步會阻塞當前線程,當同步狀態釋放時,會將首節點中的線程喚醒,使其再次嘗試獲取同步狀態。

節點的主要屬性有以下幾種:

屬性類型與名稱 描述
int waitStatus 等待狀態(如CANCELLED、SIGNAL、CONDITION、PROPAGATE、INITIAL)
Node prev 前驅節點(當節點加入同步隊列時被設置,在尾部添加)
Node next 後繼節點
Thread thread 當前獲取同步狀態的線程

節點是構成同步隊列的基礎,同步器擁有首節點和尾節點,如果沒有成功獲取同步狀態的線程將會成爲節點添加入隊列的尾部,如下圖所示: 如上圖,同步器包含了兩個節點,一個指向頭節點,一個指向尾節點,當一個線程成功獲取到鎖,而其他線程就要被加入隊列尾部,爲了保證這個過程線程安全,同步器提供了一個基於CAS的設置尾節點的方法,CAS(比較再交換,比如同步器現在保存的尾節點爲1,那麼下一個沒有獲取到鎖的線程要進入同步對立,就先判斷隊列尾部對應的是不是1,如果是1則設置成功,如果不是1則設置失敗,再次循環,直到設置成功或者線程中斷),只有設置成功後,當前節點才正式與之前的尾節點建立關聯。
同步器將節點加入到同步隊列的過程如下圖所示:

同步隊列遵循先進先出規則(FIFO),首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,會將後繼節點喚醒,而後繼節點將會在獲取同步狀態時將自己設置爲首節點,如下圖所示: 如上圖,設置首節點是通過獲取同步狀態成功的線程來完成的,由於只有一個線程能成功獲取到同步狀態,因此設置頭節點的方法並不需要使用CAS來保證線程安全,它只需要將首節點設置成爲原首節點的後繼節點並斷開原首節點的next引用即可。

獨佔式同步狀態獲取與釋放實現原理

看過上一章的應該都知道,通過同步器的acquire(int arg)方法可以嘗試獲取同步狀態, 該方法對中斷不敏感,就是說由於線程獲取同步狀態失敗之後進入同步隊列之後,後續對線程進行中斷操作時,線程不會從同步隊列中移除,代碼如下:

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

上述代碼主要完成了同步狀態獲取、節點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作,首先調用自定義同步器實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態,如果獲取失敗,則構造同步節點並通過addWaiter(Node node)方法將該節點加入同步隊列的尾部,最後調用acquireQueued(Node node, int arg)方法,使得該節點以“死循環(for(;;))”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或線程被中斷來實現
將節點添加到同步隊列尾部的代碼如下:

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

上述代碼通過使用CAS方法來確保節點能夠被線程安全添加。在enq(final Node node)方法中,同步器通過“死循環”來保證節點正確被添加,在“死循環”中只有通過CAS將節點設置爲尾節點之後,當前線程才能從該方法中返回,否則,當前線程不斷通過CAS嘗試設置。
當節點進入同步隊列中之後,就進入了一個自旋的過程,每個節點(就是每個線程)都在自省觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋中退出,否則依舊留在自旋過程中(並會阻塞當前節點的線程),代碼如下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null// help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在acquireQueued(final Node node, int arg)方法中,當前線程在死循環中嘗試獲取同步狀態,而之後前驅節點是頭節點的線程才能嘗試獲取同步狀態,原因如下:

  • 頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態之後,將會喚醒其後繼節點,後繼節點的線程被喚醒後需要檢查自己的前驅節點是否爲頭節點。
  • 維護同步隊列的先進先出原則。 由於非首節點線程前驅節點出隊或者線程被中斷從等待狀態返回,隨後檢查自己的前驅是否爲首節點,如果是則嘗試獲取同步狀態,如果不是則繼續自旋。可以看到節點與節點之間基本是不通信,而是簡單的判斷自己的前驅是否爲頭節點,這樣就使得節點釋放規則符合先進先出,並且也便於對過早通知的處理。
    acquire(int arg)方法的流程圖如下: 如上流程圖,前驅節點爲頭節點且能夠獲取同步狀態的判斷條件和線程進入等待狀態是獲取同步狀態的自旋過程。當同步狀態獲取成功之後,當前線程從acquire(int arg)方法返回。
    當線程獲取了鎖,執行相應邏輯之後就要釋放鎖,使得後續節點能夠繼續獲取同步狀態。通過調用同步器的release(int arg)方法可以釋放同步狀態, 該方法在釋放了同步狀態之後,會喚醒後繼節點(進而使後繼節點重新嘗試獲取同步狀態)。代碼如下:
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

其中unparkSuccessor(h)這一句就是用來喚醒處於等待狀態的後繼節點。

共享式式同步狀態獲取與釋放實現原理

共享式獲取與獨佔式獲取同步狀態最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態。以文件的讀寫爲例,如果一個程序在對文件進行讀操作,那麼這一時刻對於該文件的寫操作均被阻塞,而讀操作能夠同時進行。寫操作要求對資源的獨佔式訪問,而讀操作可以是共享式訪問,兩種不同的訪問模式在同一時刻對文件或資源的訪問情況如下圖所示: 如上圖所示,左邊共享式訪問資源時,其他共享式的訪問均被允許,而獨佔式訪問被阻塞,而右邊獨佔式訪問資源時,同一時刻的其他訪問均被阻塞。
通過同步器的acquireShared(int arg)方法可以共享式的獲取同步狀態,代碼如下:

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null// help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在acquireShared(int arg)方法中,同步器調用tryAcquireShared(int arg)方法嘗試獲取同步狀態,當返回值大於0時,表示能夠獲取到同步狀態。因此,在共享式獲取的自旋過程中,成功獲取到同步狀態並退出自旋的條件就是tryAcquireShared(int arg)方法返回值大於等於0。可以看到,在doAcquireShared(int arg)方法的自旋過程中,如果當前節點的前驅爲頭節點時,嘗試獲取同步狀態,如果返回值大於等於0,表示該次獲取同步狀態成功並從自旋過程中退出。
與獨佔式一樣,共享式也需要釋放同步狀態,通過調用releaseShared(int arg)方法可以釋放同步狀態,該方法代碼如下:

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

該方法在釋放同步狀態之後,會喚醒後續處於等待狀態的節點。對於能支持多個線程同時訪問的併發組件,它和獨佔式主要區別在於tryReleaseShared(int arg)方法必須確保同步狀態線程安全釋放(想一下就明白了,獨佔式是一個線程去釋放鎖,而共享式是多個線程去釋放鎖,所以要保證每一個線程都釋放鎖成功),一般是通過循環和CAS來保證的

超時獲取同步狀態實現原理

通過調用同步器的tryAcquireSharedNanos(int arg, long nanosTimeout)方法可以超時獲取同步狀態, 即在指定的時間段內獲取同步狀態,如果過去到則返回true,否則返回false。該方法提供了傳統Java同步操作(比如synchronized關鍵字)不具備的特性。
超時獲取同步狀態過程可以被視爲相應中斷獲取同步狀態過程的“增強版”,tryAcquireSharedNanos(int arg, long nanosTimeout)方法在支持相應中斷的基礎上,增強了超時獲取的特性。針對超時通知獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,爲了防止過早通知,nanosTimeout的計算公式爲:nanosTimeout = now - lastTime,其中now爲當前喚醒時間,lastTime爲上次喚醒時間,如果nanosTimeout大於0則表示超時時間未到,需要繼續睡眠nanosTimeout納秒,反之,表示已經超時,即返回false,代碼如下:

private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException 
{
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null// help GC
                        failed = false;
                        return true;
                    }
                }
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

該方法在自旋過程中,當節點的前驅節點爲頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,否則判斷是否超時(nanosTimeout小於等於0表示超時),如果沒有超時,重新計算超時間隔nanosTimeout,然後使當前線程等待nanosTimeout納秒(當已經到設置的超時時間,該線程會從LockSupport.parkNanos(this, nanosTimeout)這一行代碼進行返回)。
如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時, 將不會使該線程進行超時等待,而是進入快速的自旋過程。原因在於:非常短的超時等待無法做到十分精確,如果這時再進行超時等待,將會讓nanosTimeout的超時從整體上表現得反而不精確。因此,在超時非常短的場景下,同步器會進入無條件的快速自旋。獨佔式超時獲取同步狀態的流程圖如下所示: 從上圖可以看出,獨佔式超時獲取同步狀態tryAcquireSharedNanos(int arg, long nanosTimeout)方法和獨佔式獲取同步狀態acquire(int arg)方法非常相似,其主要區別在於未獲取到同步狀態時的處理邏輯。acquire(int arg)在未獲取到同步狀態時,會將線程一直處於等待狀態。而tryAcquireSharedNanos(int arg, long nanosTimeout)方法會使當前線程等待nanosTimeout納秒,如果在這段時間內還沒有獲取到同步狀態,將會從等待邏輯中自動返回。

結尾

如果看了前一篇文章,就應該對AQS不陌生了,有很多鎖使基於AQS來實現的,比如讀寫鎖、計數器、可重入鎖等,看懂了AQS之後再去看其他鎖就會簡單的,四個字概括:大道本源。你連源頭都掌握了,還怕掌握不了其他的嗎?
最後,如果大家有需要的話可以關注一下我的公衆號,會即時更新Java相關技術文章,公衆號內還有一些實用資料,如Java秒殺系統視頻教程、黑馬2019的教學資料(IDEA版)、BAT面試題彙總(分類齊全)、MAC電腦常用安裝包(有一些是淘寶買的,已PJ的)。

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