AQS
是AbstractQueuedSynchronizer
的簡稱。
AbstractQueuedSynchronizer 同步狀態
AbstractQueuedSynchronizer
內部有一個state
屬性,用於指示同步的狀態:
private volatile int state;
state
的欄位是個int
型的,它的值在AbstractQueuedSynchronizer
中是沒有具體的定義的,只有子類繼承AbstractQueuedSynchronizer
那麼state
才有意義,如在ReentrantLock
中,state=0
表示資源未被鎖住,而state>=1
的時候,表示此資源已經被另外一個執行緒鎖住。
AbstractQueuedSynchronizer
中雖然沒有具體獲取、修改state
的值,但是它為子類提供一些操作state
的模板方法:
獲取狀態
protected final int getState() {
return state;
}
更新狀態
protected final void setState(int newState) {
state = newState;
}
CAS更新狀態
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS 等待佇列
AQS 等待列隊是一個雙向佇列,佇列中的成員都有一個prev
和next
成員,分別指向它前面的節點和後面的節點。
佇列節點
在AbstractQueuedSynchronizer
內部,等待佇列節點由內部靜態類Node
表示:
static final class Node {
...
}
節點模式
佇列中的節點有兩種模式:
- 獨佔節點:同一時刻只能有一個執行緒訪問資源,如
ReentrantLock
- 共享節點:同一時刻允許多個執行緒訪問資源,如
Semaphore
節點的狀態
等待佇列中的節點有五種狀態:
- CANCELLED:此節點對應的執行緒,已經被取消
- SIGNAL:此節點的下一個節點需要一個喚醒訊號
- CONDITION:當前節點正在條件等待
- PROPAGATE:共享模式下會傳播喚醒訊號,就是說當一個執行緒使用共享模式訪問資源時,如果成功訪問到資源,就會繼續喚醒等待佇列中的執行緒。
自定義同步鎖
為了便於理解,使用AQS自己實現一個簡單的同步鎖,感受一下使用AQS實現同步鎖是多麼的輕鬆。
下面的程式碼自定了一個CustomLock
類,繼承了AbstractQueuedSynchronizer
,並且還實現了Lock
介面。
CustomLock
類是一個簡單的可重入鎖,類中只需要重寫AbstractQueuedSynchronizer
中的tryAcquire
與tryRelease
方法,然後在修改少量的呼叫就可以實現一個最基本的同步鎖。
public class CustomLock extends AbstractQueuedSynchronizer implements Lock {
@Override
protected boolean tryAcquire(int arg) {
int state = getState();
if(state == 0){
if( compareAndSetState(state, arg)){
setExclusiveOwnerThread(Thread.currentThread());
System.out.println("Thread: " + Thread.currentThread().getName() + "拿到了鎖");
return true;
}
}else if(getExclusiveOwnerThread() == Thread.currentThread()){
int nextState = state + arg;
setState(nextState);
System.out.println("Thread: " + Thread.currentThread().getName() + "重入");
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
int state = getState() - arg;
if(getExclusiveOwnerThread() != Thread.currentThread()){
throw new IllegalMonitorStateException();
}
boolean free = false;
if(state == 0){
free = true;
setExclusiveOwnerThread(null);
System.out.println("Thread: " + Thread.currentThread().getName() + "釋放了鎖");
}
setState(state);
return free;
}
@Override
public void lock() {
acquire(1);
}
@Override
public void unlock() {
release(1);
}
...
}
CustomLock
是實現了Lock
介面,所以要重寫lock
和unlock
方法,不過方法的程式碼很少只需要呼叫AQS中的acquire
和release
。
然後為了演示AQS的功能寫了一個小演示程式,啟動兩根執行緒,分別命名為執行緒A
和執行緒B
,然後同時啟動,呼叫runInLock
方法,模擬兩條執行緒同時訪問資源的場景:
public class CustomLockSample {
public static void main(String[] args) throws InterruptedException {
Lock lock = new CustomLock();
new Thread(()->runInLock(lock), "執行緒A").start();
new Thread(()->runInLock(lock), "執行緒B").start();
}
private static void runInLock(Lock lock){
try {
lock.lock();
System.out.println("Hello: " + Thread.currentThread().getName());
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
訪問資源(acquire)
在CustomLock的lock方法中,呼叫了 acquire(1)
,acquire
的程式碼如下 :
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- CustomLock.tryAcquire(...):
CustomLock.tryAcquire
判斷當前執行緒是否能夠訪問同步資源 - addWaiter(...):將當前執行緒新增到等待佇列的隊尾,當前節點為獨佔模型(Node.EXCLUSIVE)
- acquireQueued(...):如果當前執行緒能夠訪問資源,那麼就會放行,如果不能那當前執行緒就需要阻塞。
- selfInterrupt:設定執行緒的中斷標記
注意: 在acquire方法中,如果tryAcquire(arg)返回true, 就直接執行完了,執行緒被放行了。所以的後面的方法呼叫acquireQueued、addWaiter都是tryAcquire(arg)返回false時才會被呼叫。
tryAcquire 的作用
tryAcquire
在AQS類中是一個直接丟擲異常的實現:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
而在我們自定義的 CustomLock 中,重寫了此方法:
@Override
protected boolean tryAcquire(int arg) {
int state = getState();
if(state == 0){
if( compareAndSetState(state, arg)){
setExclusiveOwnerThread(Thread.currentThread());
System.out.println("Thread: " + Thread.currentThread().getName() + "拿到了鎖");
return true;
}
}else if(getExclusiveOwnerThread() == Thread.currentThread()){
int nextState = state + arg;
setState(nextState);
System.out.println("Thread: " + Thread.currentThread().getName() + "重入");
return true;
}
return false;
}
tryAcquire
方法返回一個布而值,true
表示當前執行緒能夠訪問資源,false
當前執行緒不能訪問資源,所以tryAcquire
的作用:決定執行緒是否能夠訪問受保護的資源。tryAcquire
裡面的邏輯在子類可以自由發揮,AQS不關心這些,只需要知道能不能訪問受保護的資源,然後來決定執行緒是放行還是進行等待佇列(阻塞)。
因為是在多執行緒環境下執行,所以不同的執行緒執行tryAcquire
時會返回不同的值,假設執行緒A比執行緒B要快一步,先到達compareAndSetState
設定state的值成員併成功,那執行緒A就會返回true,而 B 由於state的值不為0或者compareAndSetState
執行失敗,而返回false。
執行緒B 搶佔鎖流程
上面訪問到執行緒A成功獲得了鎖,那執行緒B就會搶佔失敗,接著執行後面的方法。
執行緒的入隊
執行緒的入隊是邏輯是在addWaiter
方法中,addWaiter方法的具體邏輯也不需要說太多,如果你知道連結串列
的話,就非常容易理解了,最終的結果就是將新執行緒新增到隊尾。AQS的中有兩個屬性head
、tail
分別指定等待佇列的隊首和隊尾。
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;
}
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;
}
}
}
}
需要注意的是在enq
方法中,初始化佇列的時候,會新建一個Node
做為head
和tail
,然後在之後的迴圈中將引數node
新增到隊尾,佇列初始化完後,裡面會有兩個節點,一個是空的結點new Node()
另外一個就是對應當前執行緒的結點。
由於執行緒A在tryAcquire
時返回了true
,所以它會被直接放行,那麼只有B執行緒會進入addWaiter
方法,此時的等待佇列如下:
注意: 等待佇列內的節點都是正在等待資源的執行緒,如果一個執行緒直接能夠訪問資源,那它壓根就不需要進入等待佇列,會被放行。
執行緒B 的阻塞
執行緒B被新增到等待佇列的尾部後,會繼續執行acquireQueued
方法,這個方法就是AQS阻塞執行緒的地方,acquireQueued
方法程式碼的一些解釋:
- 外面是一個
for (;;)
無限迴圈,這個很重要 - 會重新呼叫一次
tryAcquire(arg)
判斷執行緒是否能夠訪問資源了 node.predecessor()
獲取引數node
的前一個節點shouldParkAfterFailedAcquire
判斷當前執行緒獲取鎖失敗後,需不需要阻塞parkAndCheckInterrupt()
使用LockSupport
阻塞當前執行緒,
final boolean acquireQueued(final Node node, int arg) {
boolean failed = 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);
}
}
shouldParkAfterFailedAcquire 判斷是否要阻塞
shouldParkAfterFailedAcquire
接收兩個引數:前一個節點、當前節點,它會判斷前一個節點的waitStatus
屬性,如果前一個節點的waitStatus=Node.SIGNAL
就會返回true:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
acquireQueued
方法在迴圈中會多次呼叫shouldParkAfterFailedAcquire
,在等待佇列中節點的waitStatus
的屬性預設為0,所以第一次執行shouldParkAfterFailedAcquire
會執行:
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
更新完pred.waitStatus
後,節點的狀態如下:
然後shouldParkAfterFailedAcquire
返回false,回到acquireQueued
的迴圈體中,又去搶鎖還是失敗了,又會執行shouldParkAfterFailedAcquire
,第二次迴圈時此時的pred.waitStatus
等於Node.SIGNAL
那麼就會返回true。
parkAndCheckInterrupt 阻塞執行緒
這個方法就比較直觀了, 就是將執行緒的阻塞住:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
為什麼是一個for (;;)
無限迴圈呢
先看一個for (;;)
的退出條件,只有node
的前一個節點是head
並且tryAcquire返回true時才會退出迴圈,否則的話執行緒就會被parkAndCheckInterrupt
阻塞。
執行緒被parkAndCheckInterrupt
阻塞後就不會向下面執行了,但是等到它被喚醒後,它還在for (;;)
體中,然後又會繼續先去搶佔鎖,然後如果還是失敗,那又會處於等待狀態,所以一直迴圈下去,就只有兩個結果:
- 搶到鎖退出迴圈
- 搶佔鎖失敗,等待下一次喚醒再次搶佔鎖
執行緒 A 釋放鎖
執行緒A的業務程式碼執行完成後,會呼叫CustomLock.unlock
方法,釋放鎖。unlock方法內部呼叫的release(1)
:
public void unlock() {
release(1);
}
release
是AQS類的方法,它跟acquire
相反是釋放的意思:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
方法體中的tryRelease
是不是有點眼熟,沒錯,它也是在實現CustomLock
類時重寫的方法,首先在tryRelease
中會判斷當前執行緒是不是已經獲得了鎖,如果沒有就直接丟擲異常,否則的話計算state的值,如果state為0的話就可以釋放鎖了。
protected boolean tryRelease(int arg) {
int state = getState() - arg;
if(getExclusiveOwnerThread() != Thread.currentThread()){
throw new IllegalMonitorStateException();
}
boolean free = false;
if(state == 0){
free = true;
setExclusiveOwnerThread(null);
System.out.println("Thread: " + Thread.currentThread().getName() + "釋放了鎖");
}
setState(state);
return free;
}
release
方法只做了兩件事:
- 呼叫
tryRelease
判斷當前執行緒釋放鎖是否成功 - 如果當前執行緒鎖釋放鎖成功,喚醒其他執行緒(也就是正在等待中的B執行緒)
tryRelease
返回true後,會執行if裡面的程式碼塊:
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
先回顧一下現在的等待佇列的樣子:
根據上面的圖,來走下流程:
- 首先拿到
head
屬性的物件,也就是佇列的第一個物件 - 判斷
head
不等於空,並且waitStatus!=0,很明顯現在的waitStatus是等於Node. SIGNAL
的,它的值是-1
所以if (h != null && h.waitStatus != 0)
這個if肯定是滿足條件的,接著執行unparkSuccessor(h)
:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
...
if (s != null)
LockSupport.unpark(s.thread);
}
unparkSuccessor
首先將node.waitStatus
設定為0,然後獲取node的下一個節點,最後呼叫LockSupport.unpark(s.thread)
喚醒執行緒,至此我們的B執行緒就被喚醒了。
此時的佇列又回到了,執行緒B剛剛入隊的樣子:
執行緒B 喚醒之後
執行緒A釋放鎖後,會喚醒執行緒B,回到執行緒B的阻塞點,acquireQueued
的for迴圈中:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = 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結點),然後使用if判斷
if (p == head && tryAcquire(arg))
根據現在等待佇列中的節點狀態,p == head
是返回true的,然後就是tryAcquire(arg)
了,由於執行緒A已經釋放了鎖,那現在的執行緒B自然就能獲取到鎖了,所以tryAcquire(arg)也會返回true。
設定佇列頭
線路B拿到鎖後,會呼叫setHead(node)
自己設定為佇列的頭:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
呼叫setHead(node)
後佇列會發生些變化 :
移除上一個節點
setHead(node)
執行完後,接著按上一個節點完全移除:
p.next = null;
此時的佇列:
執行緒B 釋放鎖
執行緒B 釋放鎖的流程與執行緒A基本一致,只是當前佇列中已經沒有需要喚醒的執行緒,所以不需要執行程式碼去喚醒其他執行緒:
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
h != null && h.waitStatus != 0
這裡的h.waitStatus
已經是0了,不滿足條件,不會去喚醒其他執行緒。
總結
文中通過自定義一個CustomLock
類,然後通過檢視AQS原始碼來學習AQS的部分原理。通過完整的走完鎖的獲取、釋放兩個流程,加深對AQS的理解,希望對大家有所幫助。
歡迎關注我的公眾號:架構文摘,獲得獨家整理120G的免費學習資源助力你的架構師學習之路!
公眾號後臺回覆
arch028
獲取資料: