基礎介紹
JUC中的許多併發類都繼承了AbstractQueuedSynchronizer(AQS),如CountDownLatch、ReentrantLock、ThreadLocalExcutor等。
它主要實現了對同步狀態的管理以及對阻塞執行緒進行排隊、等待通知,就拿ReetrantLock為例,它有以下的功能
- 獲取鎖
- 爭搶這把鎖卻沒有成功的這些執行緒要被存放到一個集合中
- 釋放鎖,集合中的執行緒會被喚醒重現來爭搶鎖
- 使用鎖來建立Condition物件
- .....
上述這寫功能都是依賴於AQS實現的,因為ReetrantLock是隻能被一個執行緒獲取,所以它是一把獨佔鎖,而像ReentrantReadWriteLock中的ReadLock是可以被多個執行緒共享的,也就是說它是一把共享鎖。AQS中既提供了獨佔鎖的一些底層的實現,也提供了共享鎖的實現。
所以AQS中的內容主要可以分為四部分
- CLH佇列:儲存等待執行緒,其主要是通過雙向連結串列的方式實現,CLH是它的發明者的三個大佬的名字的首字母。
- 獨佔鎖
- 共享鎖
- Condition實現
AQS程式碼概覽
AbstractQueuedSynchronizer這個類當中包含兩個內部類,其中ConditionObject就是Condition功能的主要實現,一般建立Condition的方式就是Lock.newCondition(),而我們通過檢視ReentrantLock原始碼可以發現,其實際建立的Condition就是一個ConditionObject例項。
Node是等待執行緒的載體,也就是等待執行緒所在的雙向連結串列上的節點。
AbstractQueuedSynchronizer中有大量的方法,其中類似於tryAcquire和tryAcquireShared就是"類似方法"在獨佔鎖和共享鎖中的不同實現。
下圖中是AbstractQueuedSynchronizer中的一些成員變數,其中head和tail都是一個Node變數分別用於表示隊頭和隊尾節點,state表示同步狀態,stateOffset表示state變數相對於java物件的偏移量,也就是相對於AbstractQueuedSynchronizer.class的偏移量(class也是一個物件,java中萬物皆物件),主要是用於後面使用CAS的方式給相應變數設定值、修改值等操作,headOffset、tailOffset同理,waitStatusOffset和nextOffset是相對於Node.class的偏移量。另外在AbstractQueuedSynchronizer 的父類AbstractOwnableSynchronizer中還有一個重要的變數exclusiveOwnerThread表示獨佔模式下擁有當前鎖的執行緒。
Node類解析
static final class Node {
//用於標記共享模式
static final Node SHARED = new Node();
//用於標記獨佔模式
static final Node EXCLUSIVE = null;
// waitStatus 為這個值的時候 表示執行緒已經被取消
static final int CANCELLED = 1;
// waitStatus 為這個值的時候 表示後繼執行緒需要取消阻塞
static final int SIGNAL = -1;
// waitStatus 為這個值的時候 表示執行緒處於Condition下的等待狀態
static final int CONDITION = -2;
//waitStatus 為這個值的時候 表示下個acquireShared操作將被允許
static final int PROPAGATE = -3;
/**
* 這個欄位可能有以下狀態
* SIGNAL: 該節點的後繼節點被(或即將)阻塞(通過停放),因此當前節點在
* 釋放或取消時必須解除其後繼節點的停放。為了避免競爭,獲取方法
* 必須首先表明它們需要一個訊號,然後重試原子獲取,然後在失敗時
* 阻塞。
* CANCELLED: 節點因超時或中斷而被取消
* CONDITION: 這個節點被用於condition佇列,在裝個狀態下這個節點不會被用於
* 同步佇列。
* PROPAGATE: 這個節點是被共享的
* 0: 以上都不是
*
* 這個欄位的初始值為0,且是通過cas的方式對他進行安全寫操作
*/
volatile int waitStatus;
//前置節點
volatile Node prev;
//繼承節點
volatile Node next;
//該節點擁有的執行緒
volatile Thread thread;
//可能有兩種作用
//1.獨佔模式下的condition條件下的等待節點
//2.用於判斷是共享模式
Node nextWaiter;
//是否是共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回前置節點
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
通過ReentrantLock窺探AQS獨佔鎖
下面我們通過幾個例項來探究AQS中的一些方法的實現以及在ReentrantLock中起到的作用。
最簡單的例項
下面我們就通過一個簡單的lock & unLock例項來入手
通過斷點進入,lock的具體實現在ReentrantLock的NonfairSync內部類中,這是由於我們為lock物件設定的非公平鎖。
然後我們會進入AQS中的compareAndSetState方法,它主要是通過cas的方式判斷state是否為0,是-就將其更改為1並返回true、否-不修改直接返回false,若為0就意味著這個鎖現在是沒有被任何執行緒佔有的,然後我們將它的狀態更改為1表示將其佔用。
返回是後,將AQS中的獨佔執行緒的欄位賦值為當前執行緒,然後就加鎖成功了。
然後我們進入ReentrantLock的unlock方法,這個方法的主要實現就是在AQS的release方法中
然後進入tryRelease方法,會獲取當前鎖的狀態,然後用c表示要被更改成的目標狀態,校驗後,將鎖的獨佔執行緒置為空並修改其狀態欄位state為目標狀態,到這裡鎖已經解除佔用了。
tryRelease返回為true後會判斷AQS中存不存在等待節點,如果存在則就將其喚醒(後面會看這裡的原始碼)
重入鎖例項
我們使用同一把ReentrantLock進行兩次lock操作,由於第一次和上面的簡單例項流程是一樣的所以我們只關注第二次lock和unLock
此時,由於已經lock過一次,即state=1,所以compareAndSetState(0,1)不會賦值成功,所以會進入到acquire方法,進而會首先進入到tryAcquire方法
我們會在TryAcquire中再次判斷鎖的狀態(因為在此過程中上一次lock可能被釋放),然後由於當前執行緒就是這把鎖的獨佔執行緒,所以我們是可重入這把鎖的,最後將state的值改為2代表這把鎖被當前執行緒重入了兩次。
由於tryAcquire(1)返回的是true所以!tryAcquire(1)為false導致程式不會進入acquire方法中的後續執行流程,到此,意味著第二次lock已經完成。
和簡單例項中的unlock一樣,程式會先進入release方法然後進入tryRelease方法,再這裡面因為更改後的state為1所以不會講當前鎖的獨佔執行緒設定為null(會在最後一次unlock中設定)
鎖競爭例項
鎖競爭就會涉及到等待佇列以及等待節點的阻塞與喚醒,所以它的一系列操作的複雜度相對於上面的例子要更高一些。使用以下例項來體驗一下多執行緒競爭鎖的過程。
t1會首先獲取到lock,這過程與無競爭鎖的獲取是一樣的,主要的不同點在於t2獲取鎖和t1釋放鎖的過程。
在idea中可以在下入這個位置切換除錯的執行緒
在t1執行緒獲取到鎖之後,我們切換到t2執行緒,發現idea此時已經給我們標註了lock這把鎖已經被t1佔用了。
然後會進入到acquire方法,由於此時t1已經佔用了鎖,所以state ≠ 0且擁有鎖的當前執行緒為t1≠t2所以 tryAcquire返回的是false,因此程式會進入addWaiter方法。
在這個方法中,會首先將t2執行緒封裝到一個Node物件當中,然後通過tail節點判斷佇列是否被初始化了,由於CLH佇列此時並沒有元素存在,所以會進入到enq方法進行佇列的首次初始化。
在enq中會初始化這個佇列會初始化佇列,然後將傳入的node插入到隊尾,在這裡面我們看到了for(;;)的死迴圈(優雅點可以叫做自旋),那麼它的作用是什麼呢?
在此次進入到enq中實際上for只進行了兩次,第一次給頭節點設定了一個沒有實際資料的head節點,第二次將傳入的node加入到了隊尾,那麼以上工作我們是可以在一次迴圈中完成的,就比如以下程式碼塊中的實現方式
private Node enq(final Node node) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
}
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
其實自旋是為了保證執行緒安全,在t2執行緒獲取鎖的時候可能也有其它執行緒正在爭搶lock,就比如恰好有執行緒在t2執行Node t = tail
和 compareAndSetHead(new Node())
之間的時候初始化成功了佇列設定了head節點, 那麼compareAndSetHead
就會返回false不會進入這個分支,這時候就會重新獲取tail節點再將傳入的node節點插入到tail地next中,但是,此時tail可能也會被別的執行緒更改,那麼就需要不斷地自旋嘗試修改直到成功位置,自旋結束。
addWaiter結束後會進入acquireQueued,這個方法主要是會進行鎖地爭搶以及阻塞等待,最後根據failed欄位判斷是否要取消獲取執行緒,這種情況一般就是狀態被置為了Canceled
shouldParkAfterFailedAcquire判斷節點是否應該阻塞等待,如果這個節點為SIGNAL狀態就說明該節點的後繼節點應該被阻塞,繼而會執行parkAndCheckInterrupt方法對其進行阻塞,並在它被喚醒的時候判斷此執行緒是否是被interrupt的。
正常情況下如果如果t1執行緒不unlock,那麼t2執行緒將一直阻塞在parkAndCheckInterrupt方法,當其被喚醒後會繼續自旋嘗試獲取鎖。
然後我們切換回t1執行緒,進入unlock方法,呼叫AQS的release方法,然後tryRelease裡面操作跟上面兩個例項相同不在贅述,唯一不同的是之前例項的等待佇列都為空,也就是head節點都是null,所以不會去喚醒阻塞節點,因為此時我們有t2執行緒所在的節點是被儲存到了佇列中所以,程式會進入到unparkSuccessor方法中,執行完這個方法後t2執行緒會從之前的WAIT狀態轉換為RUNNING狀態即被喚醒!
t2被喚醒後會去再次tryAcquire,成功後去執行臨界區的內容,然後正常釋放lock鎖。
結尾
上面利用ReentrantLock介紹了AQS獨佔鎖相關內容,除此之外,後面還會通過ReentrantReadWriteLock介紹共享鎖的實現、Condition的實現以及其它相關的JUC類中AQS的使用。