Java 併發之 AbstractQueuedSynchronizer

若是你讀過 JUC 中 ReentrantLock、CountDownLatch、FutureTask、Semaphore 等的源代碼,會發現其中都有一個名爲 Sync 的類,而這個類是以 AbstractQueuedSynchronizer 爲基礎的,因此說 AbstractQueuedSynchronizer 是 JUC 的基礎之一(注:CyclicBarrier 並無直接以 AQS 爲基礎)。出於知其然也要知其因此然的目的,我學習了 AQS 的實現原理,並總結成此文。 java

數據結構

在 AQS 中,有兩個重要的數據結構,一個是 volatile int state,另外一個是 class Node 組成的雙向鏈表。 node

int state

顧名思義,這個變量是用來表示 AQS 的狀態的,例如 ReentrantLock 的鎖的狀態和重入次數、FutureTask 中任務的狀態、CountDownLatch 中的 count 計數等等。這個值的更新都是由 AQS compareAndSetState 方法來實現的,而這個方法則是經過 Compare and Swap 算法實現,至於這個算法的細節就很少說了。在 JDK 中,這個算法是由 Native 方法實現的。 算法

Node 雙向鏈表

Node 是 AQS 的一個內部類,主要有 waitStatus、prev、next、thread 等這麼幾個屬性。不介紹,從名字你們也能知道這些屬性的用途。 數據結構

head

指向 Node 鏈表的頭部。可爲空、一個沒用引用線程對象的空 Node、一個引用當前佔有 AQS 的線程對象的 Node。 ide

tail

指向 Node 鏈表的尾部。 學習

工做流程

acquire

這個方法自己的代碼並不長,可是流程提及來也不簡單。 ui

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

首先嚐試調用 tryAcquire(int) 方法來獲取(鎖等等,具體獲取什麼取決於你將 AQS 應用在何種場景中)。tryAcquire 這個方法是抽象方法,具體行爲須要由子類來實現。在 ReentrantLock 內部類 Sync 實現中,這個方法經過 CAS 算法設置鎖的狀態,用 AQS 中的 state 表示鎖被重入的次數。 spa

若是 tryAcquire 成功了,那也就沒什麼了,整個 acquire 操做也就成功了。若是 tryAcquire 失敗,那就須要把當前線程作入隊操做。這個入隊操做是由 Node addWaiter(Node mode) 方法來實現的。這個方法作的事情並不複雜,就是將當前線程(由於它沒有 acquire 成功)放入隊列的尾部。若是隊列是空的,則在作入隊操做以前先初始化隊列。隊列的頭節點並不引用任何線程對象或者其引用的線程對象獲取的當前的這個 AQS。總之,head 中的線程對象引用都是沒有被掛起的(null 天然不會被掛起)。 線程

在入隊操做成功以後,會再對剛剛入隊的線程作一次 acquire 操做。這樣作的目的是爲了應對短暫競爭的場景,儘可能避免掛起線程的操做。 翻譯

final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
    setHead(node);
    p.next = null; // help GC
    failed = false;
    return interrupted;
}

上面這段代碼就是讓已入隊的線程對象作 acquire 操做。p.next = null 的目的在於當 node 所引用的節點須要回收時加快內存回收的速度。

若是剛入隊的節點沒有 acquire 成功,那這個 Node (實際上是這個 Node 所引用的線程) 十有八九將被掛起。判斷的條件是這個 Node 的 waitStatus,這個狀態必須設成 SIGNAL,就是告訴別人我要被掛起了,等大家 release 的時候記得叫一下兄弟。若是狀態等於0,那就把狀態設置成 SIGNAL。這以後便把當前線程掛起,再而後天然就沒有而後了,直到被 release。

release

接下來再說說 release 的過程。release 的過程相對簡單,和 acquire 相似,首先進行 tryRelease 操做。仍是以 ReentrantLock 爲例,tryRelease 會首先判斷當前線程是否 acquire 了 AQS,若是是,則改變 AQS 的狀態。而後在嘗試恢復一個被掛起的線程,一般是 head 的 next 節點所引用的線程對象。

State

在 AQS 中有一個 int 類型的 volatile 變量 state,使用 AQS 的類能夠自定義 state 對其的含義。例如,ReentrantLock 用 0 表示沒有線程獲取鎖,大於 0 則表示重入鎖的重入次數;Semaphore 用來表示許可數量;FutureTask 用來表示任務狀態,例如運行中、已完成等。

在擴展 AQS 時,子類須要根據本身的需求在諸如 tryAcquire 方法中使用 compareAndSetState 方法設置相應的狀態值。

獨佔模式和共享模式

處於獨佔模式下時,其餘線程試圖獲取該鎖將沒法取得成功。在共享模式下,多個線程獲取某個鎖可能(但不是必定)會得到成功。此類並不「瞭解」這些不一樣,除了機械地意識到當在共享模式下成功獲取某一鎖時,下一個等待線程(若是存在)也必須肯定本身是否能夠成功獲取該鎖。

上面這段話是摘自 JDK API。說實話,這段話說的很不明白,只看這段話我也沒明白,因此仍是去看這兩個模式如何在實際中去應用。以採用 Shared 模式使用 AQS 的 CountDownLatch 爲例,它採用 acquireShared 和 releaseShared 做爲其業務方法。如同 acquire 和 release 這兩個方法,acquireShared 和 releaseShared 也會調用 tryAcquireShared 和 tryReleaseShared 這兩個須要由子類實現的方法。

以 CountDownLatch 爲例,它的 tryAcquireShared 的實現以下:

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

是否是很是簡單。對比一下 ReentrantLock 的 tryAcquire 方法的實現,我這裏就不貼出源代碼了。但即便不看源代碼,咱們也知道,ReentrantLock 的 tryAcquire 方法是排他的。可是看一下 CountDownLatch 的 tryAcquireShared 方法的實現,徹底看不出排他性的體現。其實稍加註意就會發現,tryAcquire 和 tryAcquireShared 的方法定義存在一個巨大的不一樣,就是返回值的不一樣。tryAcquire 返回的是 boolean 類型,其分別表示 acquire 成功或失敗,而 tryAcquireShared 返回的倒是 int 類型,負、零、正表明三種含義:失敗、獨佔獲取、共享獲取。這就是 AQS 文檔中對獨佔模式和共享模式描述中的那段「可能(但不是必定)」的緣由。

經過閱讀 CountDownLatch 的源代碼和我上面的講解,我想大部分人應該都能理解 AQS 獨佔模式和共享模式的含義了。

參考資料

相關文章
相關標籤/搜索