摘要:在時,引入了包,該包中的大多數同步器都是基于來構建的。框架提供了一套通用的機制來管理同步狀態阻塞喚醒線程管理等待隊列。指針用于在結點線程被取消時,讓當前結點的前驅直接指向當前結點的后驅完成出隊動作。
本文首發于一世流云的專欄:https://segmentfault.com/blog...一、AQS簡介
AbstractQueuedSynchronizer抽象類(以下簡稱AQS)是整個java.util.concurrent包的核心。在JDK1.5時,Doug Lea引入了J.U.C包,該包中的大多數同步器都是基于AQS來構建的。AQS框架提供了一套通用的機制來管理同步狀態(synchronization state)、阻塞/喚醒線程、管理等待隊列。
我們所熟知的ReentrantLock、CountDownLatch、CyclicBarrier等同步器,其實都是通過內部類實現了AQS框架暴露的API,以此實現各類同步器功能。這些同步器的主要區別其實就是對同步狀態(synchronization state)的定義不同。
AQS框架,分離了構建同步器時的一系列關注點,它的所有操作都圍繞著資源——同步狀態(synchronization state)來展開,并替用戶解決了如下問題:
資源是可以被同時訪問?還是在同一時間只能被一個線程訪問?(共享/獨占功能)
訪問資源的線程如何進行并發管理?(等待隊列)
如果線程等不及資源了,如何從等待隊列退出?(超時/中斷)
這其實是一種典型的模板方法設計模式:父類(AQS框架)定義好骨架和內部操作細節,具體規則由子類去實現。
AQS框架將剩下的一個問題留給用戶:
什么是資源?如何定義資源是否可以被訪問?
我們來看下幾個常見的同步器對這一問題的定義:
同步器 | 資源的定義 |
---|---|
ReentrantLock | 資源表示獨占鎖。State為0表示鎖可用;為1表示被占用;為N表示重入的次數 |
CountDownLatch | 資源表示倒數計數器。State為0表示計數器歸零,所有線程都可以訪問資源;為N表示計數器未歸零,所有線程都需要阻塞。 |
Semaphore | 資源表示信號量或者令牌。State≤0表示沒有令牌可用,所有線程都需要阻塞;大于0表示由令牌可用,線程每獲取一個令牌,State減1,線程沒釋放一個令牌,State加1。 |
ReentrantReadWriteLock | 資源表示共享的讀鎖和獨占的寫鎖。state邏輯上被分成兩個16位的unsigned short,分別記錄讀鎖被多少線程使用和寫鎖被重入的次數。 |
綜上所述,AQS框架提供了以下功能:
1.1 提供一套模板框架由于并發的存在,需要考慮的情況非常多,因此能否以一種相對簡單的方法來完成這兩個目標就非常重要,因為對于用戶(AQS框架的使用者來說),很多時候并不關心內部復雜的細節。而AQS其實就是利用模板方法模式來實現這一點,AQS中大多數方法都是final或是private的,也就是說Doug Lea并不希望用戶直接使用這些方法,而是只覆寫部分模板規定的方法。
AQS通過暴露以下API來讓讓用戶自己解決上面提到的“如何定義資源是否可以被訪問”的問題:
鉤子方法 | 描述 |
---|---|
tryAcquire | 排它獲取(資源數) |
tryRelease | 排它釋放(資源數) |
tryAcquireShared | 共享獲取(資源數) |
tryReleaseShared | 共享獲取(資源數) |
isHeldExclusively | 是否排它狀態 |
還記得Lock接口中的那些鎖中斷、限時等待、鎖嘗試的方法嗎?這些方法的實現其實AQS都內置提供了。
使用了AQS框架的同步器,都支持下面的操作:
阻塞和非阻塞(例如tryLock)同步;
可選的超時設置,讓調用者可以放棄等待;
可中斷的阻塞操作。
1.3 支持獨占模式和共享模式 1.4 支持Condition條件等待Condition接口,可以看做是Obejct類的wait()、notify()、notifyAll()方法的替代品,與Lock配合使用。
AQS框架內部通過一個內部類ConditionObject,實現了Condition接口,以此來為子類提供條件等待的功能。
在本章第一部分講到,AQS利用了模板方法模式,其中大多數方法都是final或是private的,我們把這類方法稱為Skeleton Method,也就是說這些方法是AQS框架自身定義好的骨架,子類是不能覆寫的。
下面會按類別簡述一些比較重要的方法,具體實現細節及原理會在本系列后續部分詳細闡述。
CAS,即CompareAndSet,在Java中CAS操作的實現都委托給一個名為UnSafe類,關于Unsafe類,以后會專門詳細介紹該類,目前只要知道,通過該類可以實現對字段的原子操作。
方法名 | 修飾符 | 描述 |
---|---|---|
compareAndSetState | protected final | CAS修改同步狀態值 |
compareAndSetHead | private final | CAS修改等待隊列的頭指針 |
compareAndSetTail | private final | CAS修改等待隊列的尾指針 |
compareAndSetWaitStatus | private static final | CAS修改結點的等待狀態 |
compareAndSetNext | private static final | CAS修改結點的next指針 |
方法名 | 修飾符 | 描述 |
---|---|---|
enq | private | 入隊操作 |
addWaiter | private | 入隊操作 |
setHead | private | 設置頭結點 |
unparkSuccessor | private | 喚醒后繼結點 |
doReleaseShared | private | 釋放共享結點 |
setHeadAndPropagate | private | 設置頭結點并傳播喚醒 |
方法名 | 修飾符 | 描述 |
---|---|---|
cancelAcquire | private | 取消獲取資源 |
shouldParkAfterFailedAcquire | private static | 判斷是否阻塞當前調用線程 |
acquireQueued | final | 嘗試獲取資源,獲取失敗嘗試阻塞線程 |
doAcquireInterruptibly | private | 獨占地獲取資源(響應中斷) |
doAcquireNanos | private | 獨占地獲取資源(限時等待) |
doAcquireShared | private | 共享地獲取資源 |
doAcquireSharedInterruptibly | private | 共享地獲取資源(響應中斷) |
doAcquireSharedNanos | private | 共享地獲取資源(限時等待) |
方法名 | 修飾符 | 描述 |
---|---|---|
acquire | public final | 獨占地獲取資源 |
acquireInterruptibly | public final | 獨占地獲取資源(響應中斷) |
acquireInterruptibly | public final | 獨占地獲取資源(限時等待) |
acquireShared | public final | 共享地獲取資源 |
acquireSharedInterruptibly | public final | 共享地獲取資源(響應中斷) |
tryAcquireSharedNanos | public final | 共享地獲取資源(限時等待) |
方法名 | 修飾符 | 描述 |
---|---|---|
release | public final | 釋放獨占資源 |
releaseShared | public final | 釋放共享資源 |
我們在第一節中講到,AQS框架分離了構建同步器時的一系列關注點,它的所有操作都圍繞著資源——同步狀態(synchronization state)來展開因此,圍繞著資源,衍生出三個基本問題:
同步狀態(synchronization state)的管理
阻塞/喚醒線程的操作
線程等待隊列的管理
3.1 同步狀態同步狀態的定義
同步狀態,其實就是資源。AQS使用單個int(32位)來保存同步狀態,并暴露出getState、setState以及compareAndSetState操作來讀取和更新這個狀態。
/** * 同步狀態. */ private volatile int state; protected final int getState() { return state; } protected final void setState(int newState) { state = newState; } /** * 以原子的方式更新同步狀態. * 利用Unsafe類實現 */ protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }3.2 線程的阻塞/喚醒
在JDK1.5之前,除了內置的監視器機制外,沒有其它方法可以安全且便捷得阻塞和喚醒當前線程。
JDK1.5以后,java.util.concurrent.locks包提供了LockSupport類來作為線程阻塞和喚醒的工具。
等待隊列,是AQS框架的核心,整個框架的關鍵其實就是如何在并發狀態下管理被阻塞的線程。
等待隊列是嚴格的FIFO隊列,是Craig,Landin和Hagersten鎖(CLH鎖)的一種變種,采用雙向鏈表實現,因此也叫CLH隊列。
1. 結點定義
CLH隊列中的結點是對線程的包裝,結點一共有兩種類型:獨占(EXCLUSIVE)和共享(SHARED)。
每種類型的結點都有一些狀態,其中獨占結點使用其中的CANCELLED(1)、SIGNAL(-1)、CONDITION(-2),共享結點使用其中的CANCELLED(1)、SIGNAL(-1)、PROPAGATE(-3)。
結點狀態 | 值 | 描述 |
---|---|---|
CANCELLED | 1 | 取消。表示后驅結點被中斷或超時,需要移出隊列 |
SIGNAL | -1 | 發信號。表示后驅結點被阻塞了(當前結點在入隊后、阻塞前,應確保將其prev結點類型改為SIGNAL,以便prev結點取消或釋放時將當前結點喚醒。) |
CONDITION | -2 | Condition專用。表示當前結點在Condition隊列中,因為等待某個條件而被阻塞了 |
PROPAGATE | -3 | 傳播。適用于共享模式(比如連續的讀操作結點可以依次進入臨界區,設為PROPAGATE有助于實現這種迭代操作。) |
INITIAL | 0 | 默認。新結點會處于這種狀態 |
AQS使用CLH隊列實現線程的結構管理,而CLH結構正是用前一結點某一屬性表示當前結點的狀態,之所以這種做是因為在雙向鏈表的結構下,這樣更容易實現取消和超時功能。
next指針:用于維護隊列順序,當臨界區的資源被釋放時,頭結點通過next指針找到隊首結點。
prev指針:用于在結點(線程)被取消時,讓當前結點的前驅直接指向當前結點的后驅完成出隊動作。
static final class Node { // 共享模式結點 static final Node SHARED = new Node(); // 獨占模式結點 static final Node EXCLUSIVE = null; static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; /** * INITAL: 0 - 默認,新結點會處于這種狀態。 * CANCELLED: 1 - 取消,表示后續結點被中斷或超時,需要移出隊列; * SIGNAL: -1- 發信號,表示后續結點被阻塞了;(當前結點在入隊后、阻塞前,應確保將其prev結點類型改為SIGNAL,以便prev結點取消或釋放時將當前結點喚醒。) * CONDITION: -2- Condition專用,表示當前結點在Condition隊列中,因為等待某個條件而被阻塞了; * PROPAGATE: -3- 傳播,適用于共享模式。(比如連續的讀操作結點可以依次進入臨界區,設為PROPAGATE有助于實現這種迭代操作。) * * waitStatus表示的是后續結點狀態,這是因為AQS中使用CLH隊列實現線程的結構管理,而CLH結構正是用前一結點某一屬性表示當前結點的狀態,這樣更容易實現取消和超時功能。 */ volatile int waitStatus; // 前驅指針 volatile Node prev; // 后驅指針 volatile Node next; // 結點所包裝的線程 volatile Thread thread; // Condition隊列使用,存儲condition隊列中的后繼節點 Node nextWaiter; Node() { } Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } }
2. 隊列定義
對于CLH隊列,當線程請求資源時,如果請求不到,會將線程包裝成結點,將其掛載在隊列尾部。
CLH隊列的示意圖如下:
①初始狀態,隊列head和tail都指向空
②首個線程入隊,先創建一個空的頭結點,然后以自旋的方式不斷嘗試插入一個包含當前線程的新結點
/** * 以自旋的方式不斷嘗試插入結點至隊列尾部 * * @return 當前結點的前驅結點 */ private Node enq(final Node node) { for (; ; ) { Node t = tail; if (t == null) { // 如果隊列為空,則創建一個空的head結點 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }四、總結
本章簡要介紹了AQS的思想和原理,讀者可以參考Doug Lea的論文,進一步了解AQS。
直接閱讀AQS的源碼比較漫無目的,后續章節,將從ReentrantLock、CountDownLatch的使用入手,講解AQS的獨占功能和共享功能。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71522.html
摘要:整個包,按照功能可以大致劃分如下鎖框架原子類框架同步器框架集合框架執行器框架本系列將按上述順序分析,分析所基于的源碼為。后,根據一系列常見的多線程設計模式,設計了并發包,其中包下提供了一系列基礎的鎖工具,用以對等進行補充增強。 showImg(https://segmentfault.com/img/remote/1460000016012623); 本文首發于一世流云專欄:https...
摘要:開始獲取鎖終于輪到出場了,的調用過程和完全一樣,同樣拿不到鎖,然后加入到等待隊列隊尾然后,在阻塞前需要把前驅結點的狀態置為,以確保將來可以被喚醒至此,的執行也暫告一段落了安心得在等待隊列中睡覺。 showImg(https://segmentfault.com/img/remote/1460000016012467); 本文首發于一世流云的專欄:https://segmentfault...
摘要:關于接口的介紹,可以參見多線程進階二鎖框架接口。最終線程釋放了鎖,并進入阻塞狀態。當線程被通知喚醒時,則是將條件隊列中的結點轉換成等待隊列中的結點,之后的處理就和獨占功能完全一樣。 showImg(https://segmentfault.com/img/remote/1460000016012490); 本文首發于一世流云的專欄:https://segmentfault.com/bl...
摘要:公平策略在多個線程爭用鎖的情況下,公平策略傾向于將訪問權授予等待時間最長的線程。使用方式的典型調用方式如下二類原理的源碼非常簡單,它通過內部類實現了框架,接口的實現僅僅是對的的簡單封裝,參見原理多線程進階七鎖框架獨占功能剖析 showImg(https://segmentfault.com/img/remote/1460000016012582); 本文首發于一世流云的專欄:https...
摘要:好了,繼續向下執行,嘗試獲取鎖失敗后,會調用首先通過方法,將包裝成共享結點,插入等待隊列,插入完成后隊列結構如下然后會進入自旋操作,先嘗試獲取一次鎖,顯然此時是獲取失敗的主線程還未調用,同步狀態還是。 showImg(https://segmentfault.com/img/remote/1460000016012541); 本文首發于一世流云的專欄:https://segmentfa...
閱讀 2774·2021-11-22 15:11
閱讀 3537·2021-09-28 09:43
閱讀 2889·2019-08-30 13:05
閱讀 3431·2019-08-30 11:18
閱讀 1447·2019-08-29 16:34
閱讀 1300·2019-08-29 13:53
閱讀 2908·2019-08-29 11:03
閱讀 1658·2019-08-29 10:57