一、什麼是 AQS ?
AQS即AbstractQueuedSynchronizer的縮寫,是併發程式設計中實現同步器的一個框架。框架,框架,重要的事情說三遍,框架就是說它幫你處理了很大一部分的邏輯,其它功能需要你來擴充套件。想想你使用Spring框架的場景,Spring幫助開發者實現IOC容器的bean依賴管理,標籤解析等,我們只需要對bean進行配置即可,其他不用管。
AQS基於一個FIFO雙向佇列實現,被設計給那些依賴一個代表狀態的原子int值的同步器使用。我們都知道,既然叫同步器,那個肯定有個代表同步狀態(臨界資源)的東西,在AQS中即為一個叫state的int值,該值通過CAS進行原子修改。
在AQS中存在一個FIFO佇列,佇列中的節點表示被阻塞的執行緒,佇列節點元素有4種型別, 每種型別表示執行緒被阻塞的原因,這四種型別分別是:
CANCELLED
: 表示該執行緒是因為超時或者中斷原因而被放到佇列中CONDITION
: 表示該執行緒是因為某個條件不滿足而被放到佇列中,需要等待一個條件,直到條件成立後才會出隊SIGNAL
: 表示該執行緒需要被喚醒PROPAGATE
: 表示在共享模式下,當前節點執行釋放release
操作後,當前結點需要傳播通知給後面所有節點
由於一個共享資源同一時間只能由一條執行緒持有,也可以被多個執行緒持有,因此AQS中存在兩種模式,如下:
-
1、獨佔模式
獨佔模式表示共享狀態值state每次能由一條執行緒持有,其他執行緒如果需要獲取,則需要阻塞,如JUC中的
ReentrantLock
-
2、共享模式
共享模式表示共享狀態值state每次可以由多個執行緒持有,如JUC中的
CountDownLatch
二、AQS 中的核心資料結構和方法
1、既然AQS是基於一個FIFO佇列的框架,那麼我們先來看下佇列的元素節點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;
// waitStatus只取上面CANCELLED、SIGNAL、CONDITION、PROPAGATE四種取值之一
volatile int waitStatus;
// 表示前驅節點
volatile Node prev;
// 表示後繼節點
volatile Node next;
// 佇列元素需要關聯一個執行緒物件
volatile Thread thread;
// 表示下一個waitStatus值為CONDITION的節點
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() {}
/**
* 當有執行緒需要入隊時,那麼就建立一個新節點,然後關聯該執行緒物件,由addWaiter()方法呼叫
*/
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;
}
}
複製程式碼
總結下Node節點資料結構設計,佇列中的元素,肯定是為了儲存由於某種原因導致無法獲取共享資源state而被入隊的執行緒,因此Node
中使用了waitStatus
表示節點入隊的原因,使用Thread
物件來表示節點所關聯的執行緒。至於prev
,next
,則是一般雙向佇列資料結構必須提供的指標,用於對佇列進行相關操作。
2、AQS中的共享狀態值
之前提到,AQS是基於一個共享的int型別的state值來實現同步器同步的,其宣告如下:
/**
* 同步狀態值
*/
private volatile int state;
/**
* 獲取同步狀態值
*/
protected final int getState() {
return state;
}
/**
* 修改同步狀態值
*/
protected final void setState(int newState) {
state = newState;
}
複製程式碼
由原始碼我們可以看出,AQS宣告瞭一個int型別的state值,為了達到多執行緒同步的功能,必然對該值的修改必須多執行緒可見,因此,state採用volatile修飾,而且getState()
和setState()
方法採用final進行修飾,目的是限制AQS的子類只能呼叫這兩個方法對state的值進行設定和獲取,而不能對其進行重寫自定義設定/獲取邏輯。
AQS中提供對state值修改的方法不僅僅只有setState()
和getState()
,還有諸如採用CAS機制進行設定的compareAndSetState()
方法,同樣,該方法也是採用final
修飾的,不允許子類重寫,只能呼叫。
3、AQS中的tryXXX方法
一般基於AQS實現的同步器,如ReentrantLock
,CountDownLatch
等,對於state的獲取操作,子類只需重寫其tryAcquire()
和tryAcquireShared()
方法即可,這兩個方法分別對應獨佔模式和共享模式下對state的獲取操作;而對於釋放操作,子類只需重寫tryRelease()
和tryReleaseShared()
方法即可。
至於如何維護佇列的出隊、入隊操作,子類不用管,AQS已經幫你做好了。
三、AQS 設計妙處
優秀的專案總會有亮點可挖,AQS也是。小編在看了AQS的原始碼之後,結合其他作者相關部落格,總結了以下兩點感覺很優秀的設計點,這是我們應該學習的,前輩總是那麼優秀。
1、自旋鎖
當我們執行一個有確定結果的操作,同時又需要併發正確執行,通常可以採用自旋鎖實現。在AQS中,自旋鎖採用 死迴圈 + CAS
實現。針對AQS中的enq()
進行講解:
private Node enq(final Node node) {
// 死迴圈 + CAS ,解決入隊併發問題
/**
* 假設有三個執行緒同時都需要入隊操作,那麼使用死迴圈和CAS可保證併發安全,同一時間只有一個節點安全入隊,入隊失敗的執行緒則迴圈重試
*
* 1、如果不要死迴圈可以嗎?只用CAS.
* 不可以,因為如果其他執行緒修改了tail的值,導致1處程式碼返回false,那麼方法enq方法將推出,導致該入隊的節點卻沒能入隊
*
* 2、如果只用死迴圈,不需要CAS可以嗎?
* 不可以,首先不需要使用CAS,那就沒必要再使用死迴圈了,再者,如果不使用CAS,那麼當執行1處程式碼時,將會改變佇列的結構
*/
for (;;) {
// 獲取尾部節點
Node t = tail;
// 如果還沒有初始化,那麼就初始化
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
// 剛開始肯定是頭指標和尾指標相等
tail = head;
} else {
// 當前結點的前驅節點等於尾部節點
node.prev = t;
// 如果當前尾結點仍然是t,那麼執行入隊並返回true,否則返回false,然後重試
if (compareAndSetTail(t, node)) { // 1
t.next = node;
return t;
}
}
}
}
複製程式碼
首先入隊操作要求的最終結果必須是一個節點插入到佇列中去,只能成功,不能失敗!然而這個入隊的操作是需要併發執行的,有可能同時有很多的執行緒需要執行入隊操作,因此我們需要採取相關的執行緒同步機制。自旋鎖採取樂觀策略,即使用了CAS中的compareAndSet()
操作,如果某次執行返回fasle,那麼當前操作必須重試,因此,採用for死迴圈直到成功為止,成功,則break跳出for迴圈或者直接return操作退出方法。
2、模板方法
在AQS中,模板方法設計模式體現在其acquire()、release()
方法上,我們先來看下原始碼:
public final void acquire(int arg) {
// 首先嚐試獲取共享狀態,如果獲取成功,則tryAcquire()返回true
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
其中呼叫tryAcquire()
方法的預設實現是丟擲一個異常,也就是說tryAcquire()
方法留給子類去實現,acquire()
方法定義了一個模板,一套處理邏輯,相關具體執行方法留給子類去實現。
關於更多模板方法設計模式,可以查閱談一談我對‘模板方法’設計模式的理解(Template)
四、自定義自己的併發同步器
下邊以JDK
文件的一個例項進行介紹:
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();
}
}
複製程式碼
實現自己的同步類一般都會自定義同步器(sync),並且將該類定義為內部類,供自己使用;而同步類自己(Mutex)則實現某個介面,對外服務。當然,介面的實現要直接依賴sync,它們在語義上也存在某種對應關係!!而sync只用實現資源state的獲取-釋放方式tryAcquire-tryRelelase,至於執行緒的排隊、等待、喚醒等,上層的AQS都已經實現好了,我們不用關心。
除了Mutex,ReentrantLock/CountDownLatch/Semphore這些同步類的實現方式都差不多,不同的地方就在獲取-釋放資源的方式tryAcquire-tryRelelase。掌握了這點,AQS的核心便被攻破了!