一、什麼是AQS
我們常用的j.u.c包裏,提供了許多強大的同步工具,例如ReentrantLock,Semphore,ReentrantReadWriteLock等,但當這些工具難以滿足某個場景的需求時,我們就需要定製化我們自己的同步器,這時,我們可能會想,如果能有一個像Servlet這種只要重寫某幾個方法就能完成一把定製鎖的實現的就好了!! 沒錯,AQS就是提供了這樣一種功能,它如果要實現一個同步器的大部分通用功能都幫我們實現好了,然後提供出抽象函數供我們重寫來定製化自己想要的同步器。 實際上,上面所說的ReentrantLock,Semphore,ReentrantReadWriteLock等juc包中同步工具的實現,也都是在AQS的輔助下進行的“二次開發”。 例如在ReentrantLock繼承了Lock接口,然後利用定製化了的繼承了AQS的類,來去實現Lock接口。
二、AQS提供了什麼功能
同步器一般會包括兩種方法,一種是acquire方法, 另一種是release方法; acquire方法是嘗試獲取鎖操作,如果獲取不到就阻塞(park)當前線程,並將其放入等待隊列中;release方法是釋放鎖操作,然後會從等待隊列中出隊一個或多個被acquire阻塞的線程並將其喚醒(unpark).
j.u.c包中並沒有對同步器的API做一個統一的定義。因此,有一些類定義了通用的接口(如Lock),而另外一些則定義了其專有的版本。因此在不同的類中,acquire和release操作的名字和形式會各有不同。例如:Lock.lock,Semaphore.acquire,CountDownLatch.await和FutureTask.get,在這個框架裏,這些方法都是acquire操作。但是,J.U.C爲支持一系列常見的使用選項,在類間都有個一致約定。在有意義的情況下,每一個同步器都支持下面的操作:
- 阻塞(例如:acquire)和非阻塞(例如:tryAcquire)同步。
- 可選的超時設置,讓調用者可以放棄等待
- 通過中斷實現的任務取消,通常是分爲兩個版本,一個acquire可取消,而另一個不可以(例如ReentrantLock中的
lockInterruptibly()
就是可在阻塞等待中被中斷的,而lock()
是阻塞等待中不可被中斷的)。
三、讀源碼之前需要知道的知識
AQS的內部隊列
在AQS中,被阻塞的線程會被打包成一個Node然後放到等待隊列中,head指向隊列頭結點,tail指向尾結點,隊列不存在時(未初始化時)的樣子爲:head==tail==null
,初始化之後,隊列爲空的情況爲:head==tail==dummy頭結點
,如下圖所示:
head指向dummy頭結點,這個頭結點存在的意義是爲了方便隊列操作,並且裏面保存的thread恆爲null。下面來看一下node每個字段的意思
Node
爲了抓住重點學習,這裏只介紹Node裏的重要成員:
- thread :當前結點裏保存的線程
- prev,next:當前結點的前後指針,這裏隊列的實現是帶有頭結點的雙向鏈表。 prev是靠近頭結點那一端的,next是靠近尾結點那一端的。
- waitStatus:初始狀態爲0。爲-1時,表示存在正在阻塞等待的線程,結點入隊之後,會自旋一次來再次嘗試tryAcquire,如果依然失敗,纔會進入阻塞,自旋的這一次就是把waitStatus字段CAS成-1。 這一字段取值範圍如下:
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
// 當前結點爲-1, 則說明後一個結點需要park阻塞
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
四、AQS源碼解讀
這裏先更新一下獨佔式的部分。。共享式的日後再看.
一、獨佔式代碼部分
先有個宏觀上的理解,如下圖:
其中tryRelease,tryAcquire是非阻塞式獲取鎖。 有了宏觀上的框架,再去看一下實現的細節。
1. acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
這裏使用了短路原理
, 如果tryAcquire
成功的話,就直接跳出if了; 如果 tryAcquire
失敗,那麼會先執行addWaiter
把當前線程打包成一個node放入等待隊列, 然後再執行acquireQueued
嘗試一次自旋,如果依然無法獲取到鎖,就進入阻塞。
2. addWaiter
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;
}
將當前線程打包成一個node, 然後將這個node入隊,如果入隊失敗則有2種情況:
- 隊列還不存在(隊列還沒初始化)
- 在入隊時,出現了同步問題。(這裏的隊列也是臨界資源,如果CAS失敗說明資源競爭失敗)
當入隊失敗時,進入enq
函數,這一函數的作用是:初始化隊列並自旋入隊操作。
3. enq
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;
}
}
}
}
如果隊列未初始化,那麼就初始化隊列,如果已經初始化了,就將當前結點自旋入隊,該方法一定返回true.
線程被打包成結點,然後入隊之後,會進入acquireQueued進行一次自旋try,如果依然失敗就阻塞
4. acquireQueued
final boolean acquireQueued(final Node node, int arg) {
booleanfailed = 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);
}
}
先判斷前驅結點是不是head,因爲head指向的是dummy結點,因此,如果前驅結點就是head了,那麼當前結點就是隊首了!! 然後只有隊首的結點纔有資格在第一次自旋的時候進行tryAcquire
每一個結點不會改變自己的waitStatus, 只會改變在隊列中前驅結點的waitStatus , 因此,如果前驅結點是0,則通過CAS操作將其變爲-1,然後自旋一次,如果前驅結點是-1,則說明已經自旋過一次了,然後才能進入 parkAndCheckInterrupt
函數,也就是將當前結點的線程阻塞。
這個函數裏的幾個細節,如果隊首元素成功tryAcquire,則需要進行出隊操作,把當前結點設置成dummy結點就可以了。
在setHead的時候。 會將thread設置成null 也是用於help gc 。 同時也要手動讓前驅結點的next設置爲null, 方便gc回收…
到此位置,線程就會被卡在parkAndCheckInterrupt
這個函數中,等待被喚醒
5. release
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release的實現就更短了,如果tryRelease成功的話,就看是否還存在阻塞等待的線程,if (h != null && h.waitStatus != 0)
這句話的判斷就是判斷否還存在阻塞等待的線程。 如果h是null的話,則說明隊列根本就不存在,更別說等待的線程了,如果h.waitStatus不是0的話,則說明隊列裏存在等待的線程node。
如果存在正在等待的線程的話,就unparkSuccessor
, 即喚醒這個正在等待的隊首線程.
6. unparkSuccessor
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
其中,s是下一個需要被喚醒的node結點,然後後面會對其進行unpark(喚醒)操作。
五、AQS的使用
到目前位置,只是簡單過完了一遍AQS的獨佔式的acquire和release操作, 它幫我們完成了一部分同步狀態管理事情,但是最關鍵的tryAcquire
和tryRelease
其實它是一個需要我們去重寫的方法:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
一、需要做的事情
在使用AQS的時候,往往需要我們自己去重寫:
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively:如果對於當前(正調用的)線程,同步是以獨佔方式進行的,則返回 true。此方法只是 abstractqueuedsynchronizer.conditionobject 方法內進行內部調用,因此,如果不使用條件,則不需要定義它。
在實現tryAcquire的時候,我們需要對內部的status進行操作,AQS也提供給了我們關於Status操作接口,分別是:
- getState()
- setState(int)
- compareAndSetState(int, int)
源碼實現如下:
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS在使用的時候,往往是使用一個內部類繼承AQS,然後重寫上述提到的方法,然後就可以在當前類中使用這個內部類的acquire / release來實現同步了
二、使用AQS完成信號量的功能
class Mutex implements Lock, java.io.Serializable {
// 自定義同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 判斷是否鎖定狀態
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 嘗試獲取資源,立即返回。成功則返回true,否則false。
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 這裏限定只能爲1個量
if (compareAndSetState(0, 1)) {//state爲0才設置爲1,不可重入!
setExclusiveOwnerThread(Thread.currentThread());//設置爲當前線程獨佔資源
return true;
}
return false;
}
// 嘗試釋放資源,立即返回。成功則爲true,否則false。
protected boolean tryRelease(int releases) {
assert releases == 1; // 限定爲1個量
if (getState() == 0)//既然來釋放,那肯定就是已佔有狀態了。只是爲了保險,多層判斷!
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);//釋放資源,放棄佔有狀態
return true;
}
}
// 真正同步類的實現都依賴繼承於AQS的自定義同步器!
private final Sync sync = new Sync();
//lock<-->acquire。兩者語義一樣:獲取資源,即便等待,直到成功才返回。
public void lock() {
sync.acquire(1);
}
//tryLock<-->tryAcquire。兩者語義一樣:嘗試獲取資源,要求立即返回。成功則爲true,失敗則爲false。
public boolean tryLock() {
return sync.tryAcquire(1);
}
//unlock<-->release。兩者語文一樣:釋放資源。
public void unlock() {
sync.release(1);
}
//鎖是否佔有狀態
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
瞭解了AQS的原理之後,可以來趁熱打鐵的看一下ReentrantLock的加鎖實現
六、ReentrantLock的原理
這裏主要詳細介紹一下ReentrantLock對AQS的兩種實現方式:
- 公平鎖(FairSync)
- 非公平鎖(NonfairSync)
其中Sync是公平鎖和非公平鎖的抽象基類,裏面已經初步實現了一些方法,但其中的lock()
方法和tryAcquire()
方法依然是抽象的,需要子類去進行實現,而公平鎖和非公平鎖的主要區別也主要在這兩個函數中,下面來看一下。
公平鎖與非公平鎖的實現區別
1. lock操作:
公平鎖
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
非公平鎖
final void lock() {
acquire(1);
}
可以看到,非公平鎖在lock的時候會進行一次CAS操作,如果直接獲取到鎖了的話,那麼就直接繼續執行。 在臨界區的執行速度比較快的情況下,非公平鎖會比公平鎖要更快,因爲在喚醒阻塞線程的過程中,有可能有其他線程已經取得鎖然後執行完並釋放了。。
2. tryAcquire操作:
非公平鎖:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 這裏直接進行CAS , 嘗試拿鎖
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 重入時,給state加一個acquires偏移量,對應release時會減去一次
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平鎖
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 這裏會先判斷是否存在比當前線程等待更久的線程!
// 只有不存在等待的線程的時候,纔有資格去嘗試獲取鎖資源(CAS)
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 重入時,給state加一個acquires偏移量,對應release時會減去一次
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可以看出,在tryAcquire時,公平鎖會先判斷是否存在比當前線程等待的更久的線程,如果不存在這樣的線程,才能進行CAS嘗試獲取鎖; 而非公平鎖是直接進行CAS獲取鎖。
關於Interrupt
我們知道, thread1.interrupt()就是將thread1的中斷標誌位置爲1(Thread.interrupted()是檢測並清除中斷標誌,thread1.isInterrupted()是僅僅檢測thread1的中斷標誌但不清除).
ReentrantLock()
的lock()
方法,thread因等待資源而被阻塞在等待隊列中的時候,不會被打斷,而是先將這個中斷標記位記下來,然後當獲取到鎖資源之後,執行selfInterrupt()
, 也就是在獲得鎖資源後打斷自己!! 如果希望在阻塞隊列中依然可以被打斷的話,應該使用lockInterruptibly
, 這個lock操作是可以允許線程在阻塞等待時被中斷的!
到此爲止,我們看到了在ReentrantLock中對tryAcquire和tryRelease的實現,分別實現了公平競爭和非公平競爭的場景,因爲這裏的ReentrantLock是獨佔式的鎖(也就是說資源只允許被一個線程獲取,也可以理解成01信號量),所以並沒有實現 tryAcquireShared
和tryReleaseShared
這兩個方法。 實際上,我們在使用的時候也是,需要哪種模式就實現對應模式的acquire和release.
對於 tryAcquireShared
和tryReleaseShared
這兩個方法的實現例子,可以去看看Semphore的源碼,它就是隻重寫了tryAcquireShared
和tryReleaseShared
,理解完上面分析的代碼之後,去看Semphore的源碼也不會很困難了。。日後有時間再寫Semphore
的源碼記錄把。。