大家好,我是小黑,一個在網際網路苟且偷生的農民工。
在Java併發程式設計中,經常會用到鎖,除了Synchronized這個JDK關鍵字以外,還有Lock介面下面的各種鎖實現,如重入鎖ReentrantLock,還有讀寫鎖ReadWriteLock等,他們在實現鎖的過程中都是依賴與AQS來完成核心的加解鎖邏輯的。那麼AQS具體是什麼呢?
提供一個框架,用於實現依賴先進先出(FIFO)等待佇列的阻塞鎖和相關同步器(訊號量,事件等)。 該類被設計為大多數型別的同步器的有用依據,這些同步器依賴於單個原子int值來表示狀態。 子類必須定義改變此狀態的受保護方法,以及根據該物件被獲取或釋放來定義該狀態的含義。 給定這些,這個類中的其他方法執行所有排隊和阻塞機制。 子類可以保持其他狀態欄位,但只以原子方式更新int使用方法操縱值getState() , setState(int)和compareAndSetState(int, int)被跟蹤相對於同步。
上述內容來自JDK官方文件。
簡單來說,AQS是一個先進先出(FIFO)的等待佇列,主要用在一些執行緒同步場景,需要通過一個int型別的值來表示同步狀態。提供了排隊和阻塞機制。
類圖結構
從類圖可以看出,在ReentrantLock中定義了AQS的子類Sync,可以通過Sync實現對於可重入鎖的加鎖,解鎖。
AQS通過int型別的狀態state來表示同步狀態。
AQS中主要提供的方法:
acquire(int) 獨佔方式獲取鎖
acquireShared(int) 共享方式獲取鎖
release(int) 獨佔方式釋放鎖
releaseShared(int) 共享方式釋放鎖
獨佔鎖和共享鎖
關於獨佔鎖和共享鎖先給大家普及一下這個概念。
獨佔鎖指該鎖只能同時被一個執行緒持有;
共享鎖指該鎖可以被多個執行緒同時持有。
舉個生活中的例子,比如我們使用叫車軟體叫車,獨佔鎖就好比我們打快車或者專車,一輛車只能讓一個客戶打到,不能兩個客戶同時打到一輛車;共享鎖就好比打拼車,可以有多個客戶一起打到同一輛車。
AQS內部結構
我們簡單通過一張圖先來了解下AQS的內部結構。其實就是有一個佇列,這個佇列的頭結點head代表當前正在持有鎖的執行緒,後續的其他節點代表當前正在等待的執行緒。
接下來我們通過原始碼來看看AQS的加鎖和解鎖過程。先來看看獨佔鎖是如何進行加解鎖的。
獨佔鎖加鎖過程
ReentrantLock lock = new ReentrantLock();
lock.lock();
public void lock() {
// 呼叫sync的lock方法
sync.lock();
}
可以看到在ReentrantLock的lock方法中,直接呼叫了sync這個AQS子類的lock方法。
final void lock() {
// 獲取鎖
acquire(1);
}
public final void acquire(int arg) {
// 1.先嚐試獲取,如果獲取成功,則直接返回,代表加鎖成功
if (!tryAcquire(arg) &&
// 2.如果獲取失敗,則呼叫addWaiter在等待佇列中增加一個節點
// 3. 呼叫acquireQueued告訴前一個節點,在解鎖之後喚醒自己,然後執行緒進入等待狀態
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果在等待過程中被中斷,則當前執行緒中斷
selfInterrupt();
}
在獲取鎖時,基本可以分為3步:
- 嘗試獲取,如果成功則返回,如果失敗,執行下一步;
- 將當前執行緒放入等待佇列尾部;
- 標記前面等待的執行緒執行完之後喚醒當前執行緒。
/**
* 嘗試獲取鎖(公平鎖實現)
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 獲取state,初始值為0,每次加鎖成功會+1,解鎖成功-1
int c = getState();
// 當前沒有執行緒佔用
if (c == 0) {
// 判斷是否有其他執行緒排隊在本執行緒之前
if (!hasQueuedPredecessors() &&
// 如果沒有,通過CAS進行加鎖
compareAndSetState(0, acquires)) {
// 將當前執行緒設定為AQS的獨佔執行緒
setExclusiveOwnerThread(current);
return true;
}
}
// 如果當前執行緒是正在獨佔的執行緒(已持有鎖,重入)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// state+1
setState(nextc);
return true;
}
return false;
}
private Node addWaiter(Node mode) {
// 建立一個當前執行緒的Node節點
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 如果等待佇列的尾節點!=null
if (pred != null) {
// 將本執行緒對應節點的前置節點設定為原來的尾節點
node.prev = pred;
// 通過CAS將本執行緒節點設定為尾節點
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//尾節點為空,或者在CAS時失敗,則通過enq方法重新加入到尾部。(本方法內部採用自旋)
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 尾節點為空,代表等待佇列還沒有被初始化過
if (t == null) {
// 建立一個空的Node物件,通過CAS賦值給Head節點,如果失敗,則重新自旋一次,如果成功,將Head節點賦值給尾節點
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 尾節點不為空的情況,說明等待佇列已經被初始化過,將當前節點的前置節點指向尾節點
node.prev = t;
// 將當前節點CAS賦值給尾節點
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
final boolean acquireQueued(final Node node, int arg) {
// 標識是否加鎖失敗
boolean failed = true;
try {
// 是否被中斷
boolean interrupted = false;
for (;;) {
// 取出來當前節點的前一個節點
final Node p = node.predecessor();
// 如果前一個節點是head節點,那麼自己就是老二,這個時候再嘗試獲取一次鎖
if (p == head && tryAcquire(arg)) {
// 如果獲取成功,把當前節點設定為head節點
setHead(node);
p.next = null; // help GC
failed = false; // 標識加鎖成功
return interrupted;
}
// shouldParkAfterFailedAcquire 檢查並更新前置節點p的狀態,如果node節點應該阻塞就返回true
// 如果返回false,則自旋一次。
if (shouldParkAfterFailedAcquire(p, node) &&
// 當前執行緒阻塞,在阻塞被喚醒時,判斷是否被中斷
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) // 如果加鎖成功,則取消獲取鎖
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // ws == -1
/*
* 這個節點已經設定了請求釋放的狀態,所以它可以在這裡安全park.
*/
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;
}
在整個加鎖過程可以通過下圖更清晰的理解。
獨佔鎖解鎖過程
public void unlock() {
sync.release(1);
}
同樣解鎖時也是直接呼叫AQS子類sync的release方法。
public final boolean release(int arg) {
// 嘗試解鎖
if (tryRelease(arg)) {
Node h = head;
// 解鎖成功,如果head!=null並且head.ws不等0,代表有其他執行緒排隊
if (h != null && h.waitStatus != 0)
// 喚醒後續等待的節點
unparkSuccessor(h);
return true;
}
return false;
}
解鎖過程如下:
- 先嚐試解鎖,解鎖失敗則直接返回false。(理論上不會解鎖失敗,因為正在執行解鎖的執行緒一定是持有鎖的執行緒)
- 解鎖成功之後,如果有head節點並且狀態不是0,代表有執行緒被阻塞等待,則喚醒下一個等待的執行緒。
protected final boolean tryRelease(int releases) {
// state - 1
int c = getState() - releases;
// 如果當前執行緒不是獨佔AQS的執行緒,但是這時候又來解鎖,這種情況肯定是非法的。
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 如果狀態歸零,代表鎖釋放了,將獨佔執行緒設定為null
free = true;
setExclusiveOwnerThread(null);
}
// 將減1之後的狀態設定為state
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
/*
* 如果節點的ws小於0,將ws設定為0
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* 從等待佇列的尾部往前找,直到第二個節點,ws<=0的節點。
*/
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;
}
// 如果存在符合條件的節點,unpark喚醒這個節點的執行緒。
if (s != null)
LockSupport.unpark(s.thread);
}
共享鎖加鎖過程
為了實現共享鎖,AQS中專門有一套和排他鎖不同的實現,我們來看一下原始碼具體是怎麼做的。
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
// tryAcquireShared 嘗試獲取共享鎖許可,如果返回負數標識獲取失敗
// 返回0表示成功,但是已經沒有多餘的許可可用,後續不能再成功,返回正數表示後續請求也可以成功
if (tryAcquireShared(arg) < 0)
// 申請失敗,則加入到共享等待佇列
doAcquireShared(arg);
}
tryAcquireShared嘗試獲取共享許可,本方法需要在子類中進行實現。不同的實現類實現方式不一樣。
下面的程式碼是ReentrentReadWriteLock中的實現。
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 當前有獨佔執行緒正在持有許可,並且獨佔執行緒不是當前執行緒,則返回失敗(-1)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 沒有獨佔執行緒,或者獨佔執行緒是當前執行緒。
// 獲取已使用讀鎖的個數
int r = sharedCount(c);
// 判斷當前讀鎖是否應該阻塞
if (!readerShouldBlock() &&
// 已使用讀鎖小於最大數量
r < MAX_COUNT &&
// CAS設定state,每次加SHARED_UNIT標識共享鎖+1
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) { // 標識第一次加讀鎖
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 重入加讀鎖
firstReaderHoldCount++;
} else {
// 併發加讀鎖,記錄當前執行緒的讀的次數,HoldCounter中是一個ThreadLocal。
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 否則自旋嘗試獲取共享鎖
return fullTryAcquireShared(current);
}
本方法可以總結為三步:
- 如果有寫執行緒獨佔,則失敗,返回-1
- 沒有寫執行緒或者當前執行緒就是寫執行緒重入,則判斷是否讀執行緒阻塞,如果不用阻塞則CAS將已使用讀鎖個數+1
- 如果第2步失敗,失敗原因可能是讀執行緒應該阻塞,或者讀鎖達到上限,或者CAS失敗,則呼叫fullTryAcquireShared方法。
private void doAcquireShared(int arg) {
// 加入同步等待佇列,指定是SHARED型別
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 取到當前節點的前一個節點
final Node p = node.predecessor();
// 如果前一個節點是頭節點,則當前節點是第二個節點。
if (p == head) {
// 因為是FIFO佇列,所以當前節點這時可以再嘗試獲取一次。
int r = tryAcquireShared(arg);
if (r >= 0) {
// 獲取成功,把當前節點設定為頭節點。並且判斷是否需要喚醒後面的等待節點。
// 如果條件允許,就會喚醒後面的節點
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 如果前置節點不是頭結點,說明當前節點執行緒需要阻塞等待,並告知前一個節點喚醒
// 檢查並更新前置節點p的狀態,如果node節點應該阻塞就返回true
// 當前執行緒被喚醒之後,會從parkAndCheckInterrupt()執行
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
共享鎖釋放過程
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
//tryReleaseShared()嘗試釋放許可,這個方法在AQS中預設丟擲一個異常,需要在子類中實現
if (tryReleaseShared(arg)) {
// 喚醒執行緒,設定傳播狀態 WS
doReleaseShared();
return true;
}
return false;
}
AQS是很多併發場景下同步控制的基石,其中的實現相對要複雜很多,還需要多看多琢磨才能完全理解。本文也是和大家做一個初探,給大家展示了核心的程式碼邏輯,希望能有所幫助。
好的,本期內容就到這裡,我們下期見;關注公眾號【小黑說Java】更多幹貨。