JUC同步器框架AbstractQueuedSynchronizer源碼圖文分析
前提
Doug Lea大神在編寫JUC(java.util.concurrent
)包的時候引入了java.util.concurrent.locks.AbstractQueuedSynchronizer
,Abstract Queued Synchronizer,也就是"基于隊列實現的抽象同步器",一般我們稱之為AQS。其實Doug Lea大神編寫AQS是有嚴謹的理論基礎的,他的個人博客上有一篇論文《The java.util.concurrent Synchronizer Framework》,文章在http://ifeve.com上可以找到相關的譯文(《JUC同步器框架》),如果想要深入研究AQS必須要理解一下該論文的內容,然后詳細分析一下AQS的源碼實現。本文在閱讀AQS源碼的時候選用的JDK版本是JDK11。
AQS的主要功能
AQS是JUC包中用于構建鎖或者其他同步組件(信號量、事件等)的基礎框架類。AQS從它的實現上看主要提供了下面的功能:
- 同步狀態的原子性管理。
- 線程的阻塞和解除阻塞。
- 提供阻塞線程的存儲隊列。
基于這三大功能,衍生出下面的附加功能:
- 通過中斷實現的任務取消,基于線程中斷實現。
- 可選的超時設置,也就是調用者可以選擇放棄等待。
- 定義了
Condition接口
,用于支持管程形式的await/signal/signalAll操作,代替了Object
類基于JNI提供的wait/notify/notifyAll。
AQS
還根據同步狀態的不同管理方式區分為兩種不同的實現:獨占狀態的同步器和共享狀態的同步器。
JUC同步器框架原理
《The java.util.concurrent Synchronizer Framework》一文中其實有提及到同步器框架的偽代碼:
// acquire操作如下:
while (synchronization state does not allow acquire) {
enqueue current thread if not already queued;
possibly block current thread;
}
dequeue current thread if it was queued;
//release操作如下:
update synchronization state;
if (state may permit a blocked thread to acquire){
unblock one or more queued threads;
}
翻譯一下:
// acquire操作如下:
while(同步狀態申請獲取失敗){
if(當前線程未進入等待隊列){
當前線程放入等待隊列;
}
嘗試阻塞當前線程;
}
當前線程移出等待隊列
//release操作如下:
更新同步狀態
if(同步狀態足夠允許一個阻塞的線程申請獲取){
解除一個或者多個等待隊列中的線程的阻塞狀態;
}
為了實現上述操作,需要下面三個基本組件的相互協作:
- 同步狀態的原子性管理。
- 等待隊列的管理。
- 線程的阻塞與解除阻塞。
其實基本原理很簡單,但是為了應對復雜的并發場景和并發場景下程序執行的正確性,同步器框架在上面的acquire操作和release操作中使用了死循環和CAS等操作,很多時候會讓人感覺邏輯過于復雜。
同步狀態管理
AQS
內部內部定義了一個32位整型的state變量用于保存同步狀態:
/**
* The synchronization state.
*/
private volatile int state;
// 獲取state
protected final int getState() {
return state;
}
// 直接覆蓋設置state
protected final void setState(int newState) {
state = newState;
}
// CAS設置state
protected final boolean compareAndSetState(int expect, int update) {
return STATE.compareAndSet(this, expect, update);
}
同步狀態state在不同的實現中可以有不同的作用或者表示意義,它可以代表資源數、鎖狀態等等,遇到具體的場景我們再分析它表示的意義。
CLH隊列變體
CLH鎖即Craig, Landin, and Hagersten (CLH) locks,因為它底層是基于隊列實現,一般也稱為CLH隊列鎖。CLH鎖也是一種基于鏈表的可擴展、高性能、公平的自旋鎖,申請線程僅僅在本地變量上自旋,它不斷輪詢前驅的狀態,假設發現前驅釋放了鎖就結束自旋。從實現上看,CLH鎖是一種自旋鎖,能確保無饑餓性,提供先來先服務的公平性。先看簡單的CLH鎖的一個簡單實現:
public class CLHLock implements Lock {
AtomicReference<QueueNode> tail = new AtomicReference<>(new QueueNode());
ThreadLocal<QueueNode> pred;
ThreadLocal<QueueNode> current;
public CLHLock() {
current = ThreadLocal.withInitial(QueueNode::new);
pred = ThreadLocal.withInitial(() -> null);
}
@Override
public void lock() {
QueueNode node = current.get();
node.locked = true;
QueueNode pred = tail.getAndSet(node);
this.pred.set(pred);
while (pred.locked) {
}
}
@Override
public void unlock() {
QueueNode node = current.get();
node.locked = false;
current.set(this.pred.get());
}
static class QueueNode {
boolean locked;
}
// 忽略其他接口方法的實現
}
上面是一個簡單的CLH隊列鎖的實現,內部類QueueNode
只使用了一個簡單的布爾值locked屬性記錄了每個線程的狀態,如果該屬性為true,則相應的線程要么已經獲取到鎖,要么正在等待鎖,如果該屬性為false,則相應的線程已經釋放了鎖。新來的想要獲取鎖的線程必須對tail屬性調用getAndSet()
方法,使得自身成為隊列的尾部,同時得到一個指向前驅節點的引用pred,最后線程所在節點在其前驅節點的locked屬性上自旋,值得前驅節點釋放鎖。上面的實現是無法運行的,因為一旦自旋就會進入死循環導致CPU飆升,可以嘗試使用下面將要提到的LockSupport
進行改造。
CLH隊列鎖本質是使用隊列(實際上是單向鏈表)存放等待獲取鎖的線程,等待的線程總是在其所在節點的前驅節點的狀態上自旋,直到前驅節點釋放資源。從實際來看,過度自旋帶來的CPU性能損耗比較大,并不是理想的線程等待隊列實現。
基于原始的CLH隊列鎖中提供的等待隊列的基本原理,AQS
實現一種了CLH鎖隊列的變體(variant)。AQS
類的protected修飾的構造函數里面有一大段注釋用于說明AQS
實現的等待隊列的細節事項,這里列舉幾點重要的:
-
AQS
實現的等待隊列沒有直接使用CLH鎖隊列,但是參考了其設計思路,等待節點會保存前驅節點中線程的信息,內部也會維護一個控制線程阻塞的狀態值。 - 每個節點都設計為一個持有單獨的等待線程并且"帶有具體的通知方式"的監視器,這里所謂通知方式就是自定義喚醒阻塞線程的方式而已。
- 一個線程是等待隊列中的第一個等待節點的持有線程會嘗試獲取鎖,但是并不意味著它一定能夠獲取鎖成功(這里的意思是存在公平和非公平的實現),獲取失敗就要重新等待。
- 等待隊列中的節點通過prev屬性連接前驅節點,通過next屬性連接后繼節點,簡單來說,就是雙向鏈表的設計。
- CLH隊列本應該需要一個虛擬的頭節點,但是在
AQS
中沒有直接提供虛擬的頭節點,而是延遲到第一次競爭出現的時候懶創建虛擬的頭節點(其實也會創建尾節點,初始化時頭尾節點是同一個節點)。 - Condition(條件)等待隊列中的阻塞線程使用的是相同的
Node
結構,但是提供了另一個鏈表用來存放,Condition等待隊列的實現比非Condition等待隊列復雜。
線程阻塞與喚醒
線程的阻塞和喚醒在JDK1.5之前,一般只能依賴于Object
類提供的wait()
、notify()
和notifyAll()
方法,它們都是JNI方法,由JVM提供實現,并且它們必須運行在獲取監視器鎖的代碼塊內(synchronized
代碼塊中),這個局限性先不談性能上的問題,代碼的簡潔性和靈活性是比較低的。JDK1.5引入了LockSupport
類,底層是基于Unsafe
類的park()
和unpark()
方法,提供了線程阻塞和喚醒的功能,它的機制有點像只有一個允許使用資源的信號量java.util.concurrent.Semaphore
,也就是一個線程只能通過park()
方法阻塞一次,只能調用unpark()
方法解除調用阻塞一次,線程就會喚醒(多次調用unpark()
方法也只會喚醒一次),可以想象是內部維護了一個0-1的計數器。
LockSupport
類如果使用得好,可以提供更靈活的編碼方式,這里舉個簡單的使用例子:
public class LockSupportMain implements Runnable {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
private Thread thread;
private void setThread(Thread thread) {
this.thread = thread;
}
public static void main(String[] args) throws Exception {
LockSupportMain main = new LockSupportMain();
Thread thread = new Thread(main, "LockSupportMain");
main.setThread(thread);
thread.start();
Thread.sleep(2000);
main.unpark();
Thread.sleep(2000);
}
@Override
public void run() {
System.out.println(String.format("%s-步入run方法,線程名稱:%s", FORMATTER.format(LocalDateTime.now()),
Thread.currentThread().getName()));
LockSupport.park();
System.out.println(String.format("%s-解除阻塞,線程繼續執行,線程名稱:%s", FORMATTER.format(LocalDateTime.now()),
Thread.currentThread().getName()));
}
private void unpark() {
LockSupport.unpark(thread);
}
}
// 某個時刻的執行結果如下:
2019-02-25 00:39:57.780-步入run方法,線程名稱:LockSupportMain
2019-02-25 00:39:59.767-解除阻塞,線程繼續執行,線程名稱:LockSupportMain
LockSupport
類park()
方法也有帶超時的變體版本方法,有些適合使用阻塞超時的場景不妨可以使用。
獨占線程的保存
AbstractOwnableSynchronizer
是AQS
的父類,一個同步器框架有可能在一個時刻被某一個線程獨占,AbstractOwnableSynchronizer
就是為所有的同步器實現和鎖相關實現提供了基礎的保存、獲取和設置獨占線程的功能,這個類的源碼很簡單:
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
它就提供了一個保存獨占線程的變量對應的Setter和Getter方法,方法都是final修飾的,子類只能使用不能覆蓋。
CLH隊列變體的實現
這里先重點分析一下AQS
中等待隊列的節點AQS$Node
的源碼:
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;
// 等待狀態,初始值為0,其他可選值是上面的4個值
volatile int waitStatus;
// 當前節點前驅節點的引用
volatile Node prev;
// 當前節點后繼節點的引用
volatile Node next;
// 當前節點持有的線程,可能是阻塞中等待喚醒的線程
volatile Thread thread;
// 下一個等待節點
Node nextWaiter;
// 當前操作的節點是否處于共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 獲取當前節點的前驅節點,確保前驅節點必須存在,否則拋出NPE
final Node predecessor() {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 空節點,主要是首次創建隊列的時候創建的頭和尾節點使用
Node() {}
// 設置下一個等待節點,設置持有線程為當前線程
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
// 設置waitStatus,設置持有線程為當前線程
Node(int waitStatus) {
WAITSTATUS.set(this, waitStatus);
THREAD.set(this, Thread.currentThread());
}
// CAS更新waitStatus
final boolean compareAndSetWaitStatus(int expect, int update) {
return WAITSTATUS.compareAndSet(this, expect, update);
}
// CAS設置后繼節點
final boolean compareAndSetNext(Node expect, Node update) {
return NEXT.compareAndSet(this, expect, update);
}
// 設置前驅節點
final void setPrevRelaxed(Node p) {
PREV.set(this, p);
}
// 下面是變量句柄的實現,在VarHandle出現之前使用的是Unsafe,其實底層還是照樣使用Unsafe
private static final VarHandle NEXT;
private static final VarHandle PREV;
private static final VarHandle THREAD;
private static final VarHandle WAITSTATUS;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
NEXT = l.findVarHandle(Node.class, "next", Node.class);
PREV = l.findVarHandle(Node.class, "prev", Node.class);
THREAD = l.findVarHandle(Node.class, "thread", Thread.class);
WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
}
其中,變量句柄(VarHandle)是JDK9引用的新特性,其實底層依賴的還是Unsafe
的方法,總體和JDK8的實現是基本一致。這里需要關注一下Node
里面的幾個屬性:
- waitStatus:當前
Node
實例的等待狀態,可選值有5個。- 初始值整數0:當前節點如果不指定初始化狀態值,默認值就是0,側面說明節點正在等待隊列中處于等待狀態。
-
Node#CANCELLED
整數值1:表示當前節點實例因為超時或者線程中斷而被取消,等待中的節點永遠不會處于此狀態,被取消的節點中的線程實例不會阻塞。 -
Node#SIGNAL
整數值-1:表示當前節點的后繼節點是(或即將是)阻塞的(通過park
),當它釋放或取消時,當前節點必須unpark
它的后繼節點。 -
Node#CONDITION
整數值-2:表示當前節點是條件隊列中的一個節點,當它轉換為同步隊列中的節點的時候,狀態會被重新設置為0。 -
Node#PROPAGATE
整數值-3:此狀態值通常只設置到調用了doReleaseShared()
方法的頭節點,確保releaseShared()
方法的調用可以傳播到其他的所有節點,簡單理解就是共享模式下節點釋放的傳遞標記。
- prev、next:當前
Node
實例的前驅節點引用和后繼節點引用。 - thread:當前
Node
實例持有的線程實例引用。 - nextWaiter:這個值是一個比較容易令人生疑的值,雖然表面上它稱為"下一個等待的節點",但是實際上它有三種取值的情況。
- 值為靜態實例
Node.EXCLUSIVE
(也就是null),代表當前的Node
實例是獨占模式。 - 值為靜態實例
Node.SHARED
,代表當前的Node
實例是共享模式。 - 值為非
Node.EXCLUSIVE
和Node.SHARED
的其他節點實例,代表Condition等待隊列中當前節點的下一個等待節點。
- 值為靜態實例
Node
類的等待狀態waitStatus理解起來是十分費勁的,下面分析其他源碼的時候會標識此狀態變化的時機。
其實上面的Node
類可以直接拷貝出來當成一個新建的類,然后嘗試構建一個雙向鏈表自行調試,這樣子就能深刻它的數據結構。例如:
public class AqsNode {
static final AqsNode SHARED = new AqsNode();
static final AqsNode EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile AqsNode prev;
volatile AqsNode next;
volatile Thread thread;
AqsNode nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final AqsNode predecessor() {
AqsNode p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
AqsNode() {
}
AqsNode(AqsNode nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
AqsNode(int waitStatus) {
WAITSTATUS.set(this, waitStatus);
THREAD.set(this, Thread.currentThread());
}
final boolean compareAndSetWaitStatus(int expect, int update) {
return WAITSTATUS.compareAndSet(this, expect, update);
}
final boolean compareAndSetNext(AqsNode expect, AqsNode update) {
return NEXT.compareAndSet(this, expect, update);
}
final void setPrevRelaxed(AqsNode p) {
PREV.set(this, p);
}
private static final VarHandle NEXT;
private static final VarHandle PREV;
private static final VarHandle THREAD;
private static final VarHandle WAITSTATUS;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
NEXT = l.findVarHandle(AqsNode.class, "next", AqsNode.class);
PREV = l.findVarHandle(AqsNode.class, "prev", AqsNode.class);
THREAD = l.findVarHandle(AqsNode.class, "thread", Thread.class);
WAITSTATUS = l.findVarHandle(AqsNode.class, "waitStatus", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public static void main(String[] args) throws Exception {
AqsNode head = new AqsNode();
AqsNode next = new AqsNode(AqsNode.EXCLUSIVE);
head.next = next;
next.prev = head;
AqsNode tail = new AqsNode(AqsNode.EXCLUSIVE);
next.next = tail;
tail.prev = next;
List<Thread> threads = new ArrayList<>();
for (AqsNode node = head; node != null; node = node.next) {
threads.add(node.thread);
}
System.out.println(threads);
}
}
// 某次執行的輸出:
[null, Thread[main,5,main], Thread[main,5,main]]
實際上,AQS
中一共存在兩種等待隊列,其中一種是普通的同步等待隊列,這里命名為Sync-Queue,另一種是基于Sync-Queue實現的條件等待隊列,這里命名為Condition-Queue。
Sync-Queue
前面已經介紹完AQS
的同步等待隊列節點類,下面重點分析一下同步等待隊列的相關源碼,下文的Sync隊列、同步隊列和同步等待隊列是同一個東西。首先,我們通過分析Node
節點得知Sync隊列一定是雙向鏈表,AQS
中有兩個瞬時成員變量用來存放頭節點和尾節點:
// 頭節點引用
private transient volatile Node head;
// 尾節點引用
private transient volatile Node tail;
// 變量句柄相關,用于CAS操作頭尾節點
private static final VarHandle STATE;
private static final VarHandle HEAD;
private static final VarHandle TAIL;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
STATE = l.findVarHandle(AbstractQueuedSynchronizer.class, "state", int.class);
HEAD = l.findVarHandle(AbstractQueuedSynchronizer.class, "head", Node.class);
TAIL = l.findVarHandle(AbstractQueuedSynchronizer.class, "tail", Node.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
// 確保LockSupport類已經初始化 - 這里應該是為了修復之前一個因為LockSupport未初始化導致的BUG
Class<?> ensureLoaded = LockSupport.class;
}
// 初始化同步隊列,注意初始化同步隊列的時候,頭尾節點都是指向同一個新的Node實例
private final void initializeSyncQueue() {
Node h;
if (HEAD.compareAndSet(this, null, (h = new Node())))
tail = h;
}
// CAS設置同步隊列的尾節點
private final boolean compareAndSetTail(Node expect, Node update) {
return TAIL.compareAndSet(this, expect, update);
}
// 設置頭節點,重點注意這里:傳入的節點設置成頭節點之后,前驅節點和持有的線程會置為null,這是因為:
// 1.頭節點一定沒有前驅節點。
// 2.當節點被設置為頭節點,它所在的線程一定是已經解除了阻塞。
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
當前線程加入同步等待隊列和同步等待隊列的初始化是同一個方法,前文提到過:同步等待隊列的初始化會延遲到第一次可能出現競爭的情況,這是為了避免無謂的資源浪費,具體方法是addWaiter(Node mode)
:
// 添加等待節點到同步等待隊列,實際上初始化隊列也是這個方法完成的
private Node addWaiter(Node mode) {
// 基于當前線程創建一個新節點,節點的模式由調用者決定
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
// 尾節點不為空說明隊列已經初始化過,則把新節點加入到鏈表中,作為新的尾節點,建立和前驅節點的關聯關系
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
// 尾節點為空說明隊列尚未初始化過,進行一次初始化操作
initializeSyncQueue();
}
}
}
在首次調用addWaiter()
方法,死循環至少執行兩輪再跳出,因為同步隊列必須初始化完成后(第一輪循環),然后再把當前線程所在的新節點實例添加到等待隊列中再返回(第二輪循環)當前的節點,這里需要注意的是新加入同步等待隊列的節點一定是添加到隊列的尾部并且會更新AQS
中的tail屬性為最新入隊的節點實例。
假設我們使用Node.EXCLUSIVE
模式入隊列,手上有三個線程分別是thread-1、thread-2和thread-3,線程入隊的時候都處于阻塞狀態,模擬一下依次調用上面的入隊方法的同步隊列的整個鏈表的狀態。
先是線程thread-1加入等待隊列:
[圖片上傳失敗...(image-46298-1554614211606)]
接著是線程thread-2加入等待隊列:
[圖片上傳失敗...(image-114022-1554614211606)]
最后是線程thread-3加入等待隊列:
[圖片上傳失敗...(image-303488-1554614211606)]
如果仔細研究會發現,如果所有的入隊線程都處于阻塞狀態的話,新入隊的線程總是添加到隊列的tail節點,阻塞的線程總是"爭搶"著成為head節點,這一點和CLH隊列鎖的阻塞線程總是基于前驅節點自旋以獲取鎖的思路是一致的。下面將會分析的獨占模式與共享模式,線程加入等待隊列都是通過addWaiter()
方法。
Condition-Queue
前面已經相對詳細地介紹過同步等待隊列,在AQS
中還存在另外一種相對特殊和復雜的等待隊列-條件等待隊列。介紹條件等待隊列之前,要先介紹java.util.concurrent.locks.Condition
接口。
public interface Condition {
// 當前線程進入等待狀態直到被喚醒或者中斷
void await() throws InterruptedException;
// 當前線程進入等待狀態,不響應中斷,阻塞直到被喚醒
void awaitUninterruptibly();
// 當前線程進入等待狀態直到被喚醒或者中斷,阻塞帶時間限制
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 當前線程進入等待狀態直到被喚醒或者中斷,阻塞帶時間限制
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 當前線程進入等待狀態直到被喚醒或者中斷,阻塞帶時間限制
boolean awaitUntil(Date deadline) throws InterruptedException;
// 喚醒單個阻塞線程
void signal();
// 喚醒所有阻塞線程
void signalAll();
}
Condition
可以理解為Object
中的wait()
、notify()
和notifyAll()
的替代品,因為Object
中的相應方法是JNI(Native)方法,由JVM實現,對使用者而言并不是十分友好(可能需要感知JVM的源碼實現),而Condition
是基于數據結構和相應算法實現對應的功能,我們可以從源碼上分析其實現。
Condition
的實現類是AQS
的公有內部類ConditionObject
。ConditionObject
提供的入隊列方法如下:
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */ - 條件隊列的第一個節點
private transient Node firstWaiter;
/** Last node of condition queue. */ - 條件隊列的最后一個節點
private transient Node lastWaiter;
// 公有構造函數
public ConditionObject() { }
// 添加條件等待節點
private Node addConditionWaiter() {
// 這里做一次判斷,當前線程必須步入此同步器實例
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 臨時節點t賦值為lastWaiter引用
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
// 最后一個節點不為條件等待狀態,則是取消狀態
if (t != null && t.waitStatus != Node.CONDITION) {
// 解除所有取消等待的節點的連接
unlinkCancelledWaiters();
t = lastWaiter;
}
// 基于當前線程新建立一個條件等待類型的節點
Node node = new Node(Node.CONDITION);
// 首次創建Condition的時候,最后一個節點臨時引用t為null,則把第一個節點置為新建的節點
if (t == null)
firstWaiter = node;
else
// 已經存在第一個節點,則通過nextWaiter連接新的節點
t.nextWaiter = node;
// 最后一個節點的引用更新為新節點的引用
lastWaiter = node;
return node;
}
// 從條件等待隊列解除所有取消等待的節點的連接,其實就是所有取消節點移除的操作,涉及到雙向鏈表的斷鏈操作、第一個和最后一個節點的引用更新
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 注意這里等待狀態的判斷
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
// 當前同步器實例持有的線程是否當前線程(currentThread())
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
// 暫時不分析其他方法
}
實際上,Condition
的所有await()
方法變體都調用addConditionWaiter()
添加阻塞線程到條件隊列中。我們按照分析同步等待隊列的情況,分析一下條件等待隊列。正常情況下,假設有2個線程thread-1和thread-2進入條件等待隊列,都處于阻塞狀態。
先是thread-1進入條件隊列:
[圖片上傳失敗...(image-f37b4b-1554614211606)]
然后是thread-2進入條件隊列:
[圖片上傳失敗...(image-5c7cdf-1554614211606)]
條件等待隊列看起來也并不復雜,但是它并不是單獨存在和使用的,一般依賴于同步等待隊列,下面的一節分析Condition的實現的時候再詳細分析。
獨占模式與共享模式
前文提及到,同步器涉及到獨占模型和共享模式。下面就針對這兩種模式詳細分析一下AQS
的具體實現源碼。
獨占模式
AQS
同步器如果使用獨占(EXCLUSIVE)模式,那么意味著同一個時刻,只有節點所在一個線程獲取(acuqire)原子狀態status成功,此時該線程可以從阻塞狀態解除繼續運行,而同步等待隊列中的其他節點持有的線程依然處于阻塞狀態。獨占模式同步器的功能主要由下面的四個方法提供:
-
acquire(int arg)
;申請獲取arg個原子狀態status(申請成功可以簡單理解為status = status - arg
)。 -
acquireInterruptibly(int arg)
:申請獲取arg個原子狀態status,響應線程中斷。 -
tryAcquireNanos(int arg, long nanosTimeout)
:申請獲取arg個原子狀態status,帶超時的版本。 -
release(int arg)
:釋放arg個原子狀態status(釋放成功可以簡單理解為status = status + arg
)。
獨占模式下,AQS
同步器實例初始化時候傳入的status值,可以簡單理解為"允許申請的資源數量的上限值",下面的acquire
類型的方法暫時稱為"獲取資源",而release
方法暫時稱為"釋放資源"。接著我們分析前面提到的四個方法的源碼,先看acquire(int arg)
:
public final void acquire(int arg) {
// 獲取資源成功或者新增一個獨占類型節點到同步等待隊列成功則直接返回,否則中斷當前線程
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 此方法必須又子類覆蓋,用于決定是否獲取資源成功
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 中斷當前線程
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
// 不可中斷的獨占模式下,同步等待隊列中的線程獲取資源的方法
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
// 獲取新入隊節點的前驅節點
final Node p = node.predecessor();
// 前驅節點為頭節點并且嘗試獲取資源成功,也就是每一輪循環都會調用tryAcquire嘗試獲取資源,除非阻塞或者跳出循環
if (p == head && tryAcquire(arg)) {
// 設置新入隊節點為頭節點,原來的節點會從隊列中斷開
setHead(node);
p.next = null; // help GC
return interrupted; // <== 注意,這個位置是跳出死循環的唯一位置
}
// 判斷是否需要阻塞當前獲取資源失敗的節點中持有的線程
if (shouldParkAfterFailedAcquire(p, node))
// 阻塞當前線程,如果被喚醒則返回并清空線程的中斷標記
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
/**
* 檢查并且更新獲取資源失敗的節點的狀態,返回值決定線程是否需要被阻塞。
* 這個方法是所有循環獲取資源方法中信號控制的主要方法
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 這里記住ws是當前處理節點的前驅節點的等待狀態
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驅節點狀態設置成Node.SIGNAL成功,等待被release調用釋放,后繼節點可以安全地進入阻塞狀態
return true;
if (ws > 0) {
// ws大于0只有一種情況Node.CANCELLED,說明前驅節點已經取消獲取資源,
// 這個時候會把所有這類型取消的前驅節點移除,找到一個非取消的節點重新通過next引用連接當前節點
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 其他等待狀態直接修改前驅節點等待狀態為Node.SIGNAL
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
// 阻塞當前線程,獲取并且重置線程的中斷標記位
private final boolean parkAndCheckInterrupt() {
// 這個就是阻塞線程的實現,依賴Unsafe的API
LockSupport.park(this);
return Thread.interrupted();
}
上面的代碼雖然看起來能基本理解,但是最好用圖推敲一下"空間上的變化":
[圖片上傳失敗...(image-4b9a97-1554614211606)]
[圖片上傳失敗...(image-f5243d-1554614211606)]
接著分析一下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;
}
// 嘗試釋放資源,獨占模式下,嘗試通過重新設置status的值從而實現釋放資源的功能
// 這個方法必須由子類實現
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
// 解除傳入節點(一般是頭節點)的第一個后繼節點的阻塞狀態,當前處理節點的等待狀態會被CAS更新為0
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 當前處理的節點(一般是頭節點)狀態小于0則直接CAS更新為0
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 如果節點的第一個后繼節點為null或者等待狀態大于0(取消),則從等待隊列的尾節點向前遍歷,
// 找到最后一個不為null,并且等待狀態小于等于0的節點
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
// 解除上面的搜索到的節點的阻塞狀態
if (s != null)
LockSupport.unpark(s.thread);
}
接著用上面的圖:
[圖片上傳失敗...(image-e4c83-1554614211606)]
上面圖中thread-2晉升為頭節點的第一個后繼節點,等待下一個release()
釋放資源喚醒之就能晉升為頭節點,一旦晉升為頭節點也就是意味著可以解除阻塞繼續運行。接著我們可以看acquire()
的響應中斷版本和帶超時的版本。先看acquireInterruptibly(int arg)
:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
// 獲取并且清空線程中斷標記位,如果是中斷狀態則直接拋InterruptedException異常
if (Thread.interrupted())
throw new InterruptedException();
// 如果獲取資源失敗
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
// 獨占模式下響應中斷的獲取資源方法
private void doAcquireInterruptibly(int arg) throws InterruptedException {
// 基于當前線程新增一個獨占的Node節點進入同步等待隊列中
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return;
}
// 獲取資源失敗進入阻塞狀態
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 解除阻塞后直接拋出InterruptedException異常
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
doAcquireInterruptibly(int arg)
方法和acquire(int arg)
類似,最大的不同點在于阻塞線程解除阻塞后并不是正常繼續運行,而是直接拋出InterruptedException
異常。最后看tryAcquireNanos(int arg, long nanosTimeout)
的實現:
// 獨占模式下嘗試在指定超時時間內獲取資源,響應線程中斷
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
// 獨占模式下帶超時時間限制的獲取資源方法
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
// 超時期限小于0納秒,快速失敗
if (nanosTimeout <= 0L)
return false;
// 超時的最終期限是當前系統時鐘納秒+外部指定的nanosTimeout增量
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return true;
}
// 計算出剩余的超時時間
nanosTimeout = deadline - System.nanoTime();
// 剩余超時時間小于0說明已經超時則取消獲取
if (nanosTimeout <= 0L) {
cancelAcquire(node);
return false;
}
// 這里會判斷剩余超時時間大于1000納秒的時候才會進行帶超時期限的線程阻塞,否則會進入下一輪獲取嘗試
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
tryAcquireNanos(int arg, long nanosTimeout)
其實和doAcquireInterruptibly(int arg)
類似,它們都響應線程中斷,不過tryAcquireNanos()
在獲取資源的每一輪循環嘗試都會計算剩余可用的超時時間,只有同時滿足獲取失敗需要阻塞并且剩余超時時間大于SPIN_FOR_TIMEOUT_THRESHOLD(1000納秒)
的情況下才會進行阻塞。
獨占模式的同步器的一個顯著特點就是:頭節點的第一個有效(非取消)的后繼節點,總是嘗試獲取資源,一旦獲取資源成功就會解除阻塞并且晉升為頭節點,原來所在節點會移除出同步等待隊列,原來的隊列長度就會減少1,然后頭結點的第一個有效的后繼節點繼續開始競爭資源。
[圖片上傳失敗...(image-58d39f-1554614211606)]
使用獨占模式同步器的主要類庫有:
- 可重入鎖
ReentrantLock
。 - 讀寫鎖
ReentrantReadWriteLock
中的寫鎖WriteLock
。
共享模式
共享(SHARED)模式中的"共享"的含義是:同一個時刻,如果有一個節點所在線程獲取(acuqire)原子狀態status成功,那么它會解除阻塞被喚醒,并且會把喚醒狀態傳播到所有的后繼節點(換言之就是喚醒整個同步等待隊列中的所有節點)。共享模式同步器的功能主要由下面的四個方法提供:
-
acquireShared(int arg)
;申請獲取arg個原子狀態status(申請成功可以簡單理解為status = status - arg
)。 -
acquireSharedInterruptibly(int arg)
:申請獲取arg個原子狀態status,響應線程中斷。 -
tryAcquireSharedNanos(int arg, long nanosTimeout)
:申請獲取arg個原子狀態status,帶超時的版本。 -
releaseShared(int arg)
:釋放arg個原子狀態status(釋放成功可以簡單理解為status = status + arg
)。
先看acquireShared(int arg)
的源碼:
// 共享模式下獲取資源
public final void acquireShared(int arg) {
// 注意tryAcquireShared方法值為整型,只有小于0的時候才會加入同步等待隊列
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// 共享模式下嘗試獲取資源,此方法需要由子類覆蓋
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
// 共享模式下獲取資源和處理同步等待隊列的方法
private void doAcquireShared(int arg) {
// 基于當前線程新建一個標記為共享的新節點
final Node node = addWaiter(Node.SHARED);
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
// 如果當前節點的前驅節點是頭節點
if (p == head) {
// 每一輪循環都會調用tryAcquireShared嘗試獲取資源,除非阻塞或者跳出循環
int r = tryAcquireShared(arg);
if (r >= 0) { // <= tryAcquireShared方法>=0說明直資源獲取成功
// 設置頭結點,并且傳播獲取資源成功的狀態,這個方法的作用是確保喚醒狀態傳播到所有的后繼節點
// 然后任意一個節點晉升為頭節點都會喚醒其第一個有效的后繼節點,起到一個鏈式釋放和解除阻塞的動作
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
// 判斷獲取資源失敗是否需要阻塞,這里會把前驅節點的等待狀態CAS更新為Node.SIGNAL
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
} finally {
if (interrupted)
selfInterrupt();
}
}
// 設置同步等待隊列的頭節點,判斷當前處理的節點的后繼節點是否共享模式的節點,如果共享模式的節點,
// propagate大于0或者節點的waitStatus為PROPAGATE則進行共享模式下的釋放資源
private void setHeadAndPropagate(Node node, int propagate) {
// h為頭節點的中間變量
Node h = head;
// 設置當前處理節點為頭節點
setHead(node);
// 這個判斷條件比較復雜:入參propagate大于0 || 頭節點為null || 頭節點的狀態為非取消 || 再次獲取頭節點為null || 再次獲取頭節點不為取消
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 當前節點(其實已經成為頭節點)的第一個后繼節點為null或者是共享模式的節點
if (s == null || s.isShared())
doReleaseShared();
}
}
// Release action for shared mode:共享模式下的釋放資源動作
private void doReleaseShared() {
for (;;) {
Node h = head;
// 頭節點不為null并且不為尾節點
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果頭節點等待狀態為SIGNAL(-1)則CAS更新它為0,更新成功后喚醒和解除其后繼節點的阻塞
if (ws == Node.SIGNAL) {
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue;
// 喚醒頭節點的后繼節點
unparkSuccessor(h);
}
// 如果頭節點的等待狀態為0,則CAS更新它為PROPAGATE(-3)
else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue;
}
// 頭節點沒有變更,則跳出循環
if (h == head)
break;
}
}
其實代碼的實現和獨占模式有很多類似的地方,一個很大的不同點是:共享模式同步器當節點獲取資源成功晉升為頭節點之后,它會把自身的等待狀態通過CAS更新為Node.PROPAGATE
,下一個加入等待隊列的新節點會把頭節點的等待狀態值更新回Node.SIGNAL
,標記后繼節點處于可以被喚醒的狀態,如果遇上資源釋放,那么這個阻塞的節點就能被喚醒解除阻塞。我們還是畫圖理解一下,先假設tryAcquireShared(int arg)
總是返回小于0的值,入隊兩個阻塞的線程thread-1和thread-2,然后進行資源釋放確保tryAcquireShared(int arg)
總是返回大于0的值:
[圖片上傳失敗...(image-d711f1-1554614211606)]
看起來和獨占模式下的同步等待隊列差不多,實際上真正不同的地方在于有節點解除阻塞和晉升為頭節點的過程。因此我們可以先看releaseShared(int arg)
的源碼:
// 共享模式下釋放資源
public final boolean releaseShared(int arg) {
// 嘗試釋放資源成功則調用前面分析過的doReleaseShared以傳播喚醒狀態和unpark頭節點的后繼節點
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// 共享模式下嘗試釋放資源,必須由子類覆蓋
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
releaseShared(int arg)
就是在tryReleaseShared(int arg)
調用返回true的情況下主動調用一次doReleaseShared()
從而基于頭節點傳播喚醒狀態和unpark
頭節點的后繼節點。接著之前的圖:
[圖片上傳失敗...(image-54bc69-1554614211606)]
[圖片上傳失敗...(image-4ab50f-1554614211606)]
接著看acquireSharedInterruptibly(int arg)
的源碼實現:
// 共享模式下獲取資源的方法,響應線程中斷
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
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
return;
}
}
// 和非響應線程中斷的acquireShared方法類似,不過這里解除阻塞之后直接拋出異常InterruptedException
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
最后看tryAcquireSharedNanos(int arg, long nanosTimeout)
的源碼實現:
// 共享模式下獲取資源的方法,帶超時時間版本
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 注意這里只要tryAcquireShared >= 0或者doAcquireSharedNanos返回true都認為獲取資源成功
return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout);
}
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);
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
return true;
}
}
//重新計算剩余的超時時間
nanosTimeout = deadline - System.nanoTime();
// 超時的情況下直接取消獲取
if (nanosTimeout <= 0L) {
cancelAcquire(node);
return false;
}
// 滿足阻塞狀態并且剩余的超時時間大于閥值1000納秒則通過LockSupport.parkNanos()阻塞線程
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
LockSupport.parkNanos(this, nanosTimeout);
// 解除阻塞后判斷線程的中斷標記并且清空標記位,如果是處于中斷狀態則拋出InterruptedException
if (Thread.interrupted())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
共享模式的同步器的一個顯著特點就是:頭節點的第一個有效(非取消)的后繼節點,總是嘗試獲取資源,一旦獲取資源成功就會解除阻塞并且晉升為頭節點,原來所在節點會移除出同步等待隊列,原來的隊列長度就會減少1,重新設置頭節點的過程會傳播喚醒的狀態,簡單來說就是喚醒一個有效的后繼節點,只要一個節點可以晉升為頭節點,它的后繼節點就能被喚醒。節點的喚醒順序遵循類似于FIFO的原則,通俗說就是先阻塞或者阻塞時間最長則先被喚醒。