一、摘要
在之前的文章中,我們介紹了 ReentrantLock、ReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore、ThreadPoolExecutor 等併發工具類的使用方式,它們在請求共享資源的時候,都能實現執行緒同步的效果。
在使用方式上稍有不同,有的是獨佔式,多個執行緒競爭時只有一個執行緒能執行方法,比如 ReentrantLock 等;有的是共享式,多個執行緒可以同時執行方法,比如:ReadWriteLock、CountDownLatch、Semaphore 等,不同的實現爭用共享資源的方式也不同。
如果仔細閱讀原始碼,會發現它們都是基於AbstractQueuedSynchronizer
這個抽象類實現的,我們簡稱 AQS。
AQS 是一個提供了原子式管理同步狀態、阻塞和喚醒執行緒功能的框架,是除了 Java 自帶的synchronized
關鍵字之外的鎖實現機制。
可以這麼說,AQS
是JUC
包下執行緒同步類的基石,也是很多面試官喜歡提問的話題,掌握AQS
原理對我們深入理解執行緒同步技術有著非常重要的意義。
本文以ReentrantLock
作為切入點,來解讀AQS
相關的知識點,最後配上簡單的應用示例來幫助大家理解 AQS,如果有描述不對的地方,歡迎大家留言指出,不勝感激!
二、ReentrantLock
在之前的執行緒系列文章中,我們介紹了ReentrantLock
的基本用法,它是一個可重入的互斥鎖,它具有與使用synchronized
關鍵字一樣的效果,並且功能更加強大,程式設計更加靈活,支援公平鎖和非公平鎖兩種模式。
使用方式也非常簡單,只需要在相應的程式碼上呼叫加鎖
和釋放鎖
方法即可,簡單示例如下!
public class Counter {
// 預設非公平鎖模式
private final Lock lock = new ReentrantLock();
public void add() {
// 加鎖
lock.lock();
try {
// 具體業務邏輯...
} finally {
// 釋放鎖
lock.unlock();
}
}
}
如果閱讀lock()
和unlock()
方法,會發現它的底層都是由AQS
來實現的。
下面,我們一起來看看這兩個方法的原始碼實現,本文原始碼內容摘取自 JDK 1.8 版本,可能不同的版本略有區別!
2.1、lock 方法原始碼
public class ReentrantLock implements Lock, java.io.Serializable {
// 同步鎖實現類
private final Sync sync;
public ReentrantLock() {
// 預設構造方法為非公平鎖實現類
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
// true:公平鎖實現類,false:非公平鎖實現類
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
// 加鎖操作
sync.lock();
}
// 非公平鎖實現類
static final class NonfairSync extends Sync {
// 加鎖操作
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
// 公平鎖實現類
static final class FairSync extends Sync {
// 加鎖操作
final void lock() {
acquire(1);
}
}
// 公平鎖和非公平鎖,都繼承自 AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
// lock 抽象方法
abstract void lock();
}
}
從原始碼上可以清晰的看到,當初始化ReentrantLock
物件時,需要指定鎖的模式。
預設構造方法是非公平鎖模式,採用的是NonfairSync
內部實現類;公平鎖模式下,則採用的是FairSync
內部實現類;這兩個內部實現類都繼承了Sync
抽象類;同時,Sync
也繼承了AbstractQueuedSynchronizer
,也就是我們上文提到的AQS
。
如果把lock()
方法的請求鏈路進行抽象,可以用如下圖進行簡要概括。
無論是非公平鎖模式還是公平鎖模式,可能最終都會呼叫AQS
的acquire()
方法,它表示透過獨佔式的方式加鎖,我們繼續往下看這個方法的原始碼,部分核心程式碼如下:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
// 透過獨佔式的方式加鎖
public final void acquire(int arg) {
// 嘗試加鎖,會回撥具體的實現類
if (!tryAcquire(arg) &&
// 如果嘗試加鎖失敗,將當前執行緒加入等待佇列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 由子類完成加鎖邏輯的實現,支援重寫該方法
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
}
從AQS
的原始碼上可以看出,acquire()
方法並不進行具體加鎖邏輯的實現,而是透過具體的實現類重寫tryAcquire()
方法來完成加鎖操作,如果加鎖失敗,會將當前執行緒加入等待佇列。
如果是非公平鎖模式,會回撥ReentrantLock
類的NonfairSync.tryAcquire()
方法;如果是公平鎖模式,會回撥ReentrantLock
類的FairSync.tryAcquire()
方法,我們繼續回看ReentrantLock
類的原始碼。
非公平鎖NonfairSync
靜態內部實現類,相關的原始碼如下!
// 非公平鎖實現類
static final class NonfairSync extends Sync {
// 加鎖操作
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 嘗試非公平方式加鎖,重寫父類 tryAcquire 方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 採用CAS方式修改執行緒同步狀態,如果成功返回true
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 支援當前執行緒,重複獲得鎖,將state值加1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
公平鎖FairSync
靜態內部實現類,相關的原始碼如下!
// 公平鎖實現類
static final class FairSync extends Sync {
// 加鎖操作
final void lock() {
acquire(1);
}
// 嘗試公平方式加鎖,重寫父類 tryAcquire 方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 1)判斷等待佇列是否有執行緒處於等待狀態,如果沒有,嘗試獲取鎖;如果有,就進入等待佇列
// 2)採用CAS方式修改執行緒同步狀態,如果成功返回true
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 支援當前執行緒,重複獲得鎖,將state值加1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
從原始碼上可以清晰的看到,無論是是公平鎖還是非公平鎖模式,都是採用compareAndSetState()
方法(簡稱CAS
)進行加鎖,如果成功就返回true
;同時支援當前執行緒重複獲得鎖,也就是之前提到的鎖可重入機制。
唯一的區別在於:公平鎖實現類多了一個hasQueuedPredecessors()
方法判斷,它的用途是判斷等待佇列是否有執行緒處於等待狀態,如果沒有,嘗試獲取鎖;如果有,就將當前執行緒存入等待佇列,依此排隊,從而保證執行緒透過公平方式獲取鎖的目的。
關於 CAS 實現原理,在之前的併發原子類文章中已經有所介紹,透過它加上volatile
修飾符可以實現一個無鎖的執行緒安全訪問操作,本文不再重複解讀,有興趣的朋友可以翻閱之前的文章。
2.2、unlock 方法原始碼
public class ReentrantLock implements Lock, java.io.Serializable {
// 同步鎖實現類
private final Sync sync;
public void unlock() {
// 釋放鎖操作
sync.release(1);
}
}
unlock()
方法的釋放鎖實現相對來說就簡單多了,整個請求鏈路可以用如下圖進行簡要概括。
當呼叫unlock()
方法時,會直接跳轉到AQS
的release()
方法上,AQS
相關的原始碼如下!
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
// 釋放鎖操作
public final boolean release(int arg) {
// 嘗試釋放鎖
if (tryRelease(arg)) {
// 從佇列頭部中獲取一個等待執行緒,並進行喚醒操作
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 由子類完成釋放鎖邏輯的實現,支援重寫該方法
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
}
與加鎖操作類似,AQS
的release()
方法並不進行具體釋放鎖邏輯的實現,而是透過具體的實現類重寫tryRelease()
方法來完成釋放鎖操作,如果釋放鎖成功,會從佇列頭部中獲取一個等待執行緒,並進行喚醒操作。
我們繼續回看ReentrantLock
類的Sync.tryRelease()
釋放鎖方法,部分核心原始碼如下:
abstract static class Sync extends AbstractQueuedSynchronizer {
// 嘗試釋放鎖
protected final boolean tryRelease(int releases) {
// 將state值進行減1操作
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
相比加鎖過程,釋放鎖要簡單的多,主要是將執行緒的同步狀態值進行自減操作。
三、AQS 原理淺析
如果仔細的研究 AQS 的原始碼,儘管實現上很複雜,但是也有規律可循。
從上到下,整個框架可以分為五層,架構可以用如下圖來描述!(圖片來自ReentrantLock 的實現看 AQS 的原理及應用 - 美團技術團隊)
當有自定義執行緒同步器接入AQS
時,只需要按需重寫第一層的方法即可,不需要關心底層的實現。
以加鎖為例,當呼叫AQS
的 API 層獲取鎖方法時,會先嚐試進行加鎖操作(具體邏輯由實現類完成),如果加鎖失敗,會進入等待佇列處理環節,這些處理邏輯同時也依賴最底層的基礎資料提供層來完成。
3.1、原理概述
整個AQS
實現執行緒同步的核心思想,可以用如下這段話來描述!
AQS 內部維護一個共享資源變數和執行緒等待佇列,如果被請求的共享資源空閒,那麼就將當前請求資源的執行緒設定為有效的工作執行緒,將共享資源設定為鎖定狀態;如果共享資源被佔用,就需要一定的阻塞等待喚醒機制來保證鎖分配。這個機制主要用的是 CLH 佇列的變體實現的,將暫時獲取不到鎖的執行緒加入到等待佇列中,待條件允許的時候將執行緒從佇列中取出並進行喚醒。
CLH 佇列是一個單向連結串列佇列,對應的還有 CLH 鎖實現,它是一個基於邏輯佇列非執行緒飢餓的一種自旋公平鎖實現,由 Craig、Landin 和 Hagersten 三位大佬發明,因此命名為 CLH 鎖。關於這方面的技術知識講解可以參閱這篇文章:多圖詳解 CLH 鎖的原理與實現。
而AQS
中的佇列採用的是 CLH 變體的虛擬雙向佇列,透過將每一條請求共享資源的執行緒封裝成一個 CLH 佇列的一個節點來實現鎖的分配。
具體實現原理,可以用如下圖來簡單概括:
同時,AQS
中維護了一個共享資源變數state
,透過它來實現執行緒的同步狀態控制,這個欄位使用了volatile
關鍵字修飾符來保證多執行緒下的可見性。
當多個執行緒嘗試獲取鎖時,會透過CAS
方式來修改state
值,當state=1
時表示當前物件鎖已經被佔有(相對獨佔模式來說),此時其他執行緒來加鎖時會失敗,加鎖失敗的執行緒會被放入上文說到的FIFO
等待佇列中,並且執行緒會被掛起,等待其他獲取鎖的執行緒釋放鎖才能夠被喚醒。
總結下來,用大白話說就是,AQS
是基於 CLH 佇列,使用volatile
修飾共享變數state
,執行緒透過CAS
方式去改變state
狀態值,如果成功則獲取鎖成功,失敗則進入等待佇列,等待被喚醒的執行緒同步器框架。
開啟 ReentrantLock、ReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore 等類的原始碼實現,你會發現它們的執行緒同步狀態都是基於AQS
實現的,可以看成是AQS
的衍生物。
下面我們一起來看看相關的原始碼實現!
3.2、原始碼淺析
3.2.1、執行緒同步狀態控制
AQS
原始碼中維護的共享資源變數state
,表示同步狀態的意思,它是實現執行緒同步控制的關鍵欄位,核心原始碼如下:
/**
* The synchronization state.
*/
private volatile int state;
針對state
欄位值的獲取和修改,AQS
提供了三個方法,並且都採用Final
修飾,意味著子類無法重寫它們,相關方法如下:
方法 | 描述 |
---|---|
protected final int getState() | 獲取state 的值 |
protected final void setState(int newState) | 設定state 的值 |
protected final boolean compareAndSetState(int expect, int update) | 使用 CAS 方式更新state |
如果仔細分析原始碼,state
欄位還有一個很大的用處,透過它可以實現多執行緒的獨佔模式和共享模式。
以ReentrantLock
和Semaphore
類為例,它們的加鎖過程中state
值的變化情況如下。
3.2.1.1、ReentrantLock 獨佔模式的獲取鎖,簡易流程圖如下:
ReentrantLock
類部分核心原始碼,實現邏輯如下:
public class ReentrantLock implements Lock, java.io.Serializable {
// 非公平鎖實現類
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 加鎖操作
final void lock() {
// 將state從0設定為1,如果成功,直接獲取當前共享資源
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 嘗試加鎖,會轉調tryAcquire方法
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 判斷state是否等於0
if (c == 0) {
// 嘗試state從0設定為1,如果成功,返回true
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 支援當前執行緒可重入,每呼叫一次,state的值加1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
}
3.2.1.2、Semaphore 共享模式的獲取鎖,簡易流程圖如下:
Semaphore
類部分核心原始碼,實現邏輯如下:
public class Semaphore implements java.io.Serializable {
// 初始化的時候,設定執行緒最大併發數,本質設定的是state的值
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
// 非公平鎖內部實現類
static final class NonfairSync extends Sync {
NonfairSync(int permits) {
// 設定state的值
setState(permits);
}
// 透過共享方式,嘗試獲取鎖
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
// 嘗試獲取共享資源,會呼叫Sync.nonfairTryAcquireShared方法
public boolean tryAcquire() {
// 如果state的值小於0,表示無可用共享資源
return sync.nonfairTryAcquireShared(1) >= 0;
}
// 抽象同步類
abstract static class Sync extends AbstractQueuedSynchronizer {
// 透過共享方式,嘗試獲取鎖
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 透過cas方式,設定state自減
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
}
3.2.2、公平鎖和非公平鎖實現
在上文的ReentrantLock
原始碼分析過程中,對於公平鎖和非公平鎖實現,其實已經有所解讀。
在AQS
中所有的加鎖邏輯是有具有的實現類來完成,以ReentrantLock
類為例,它的加鎖邏輯由兩個實現類來完成,分別是非公平鎖靜態內部實現類NonfairSync
和公平鎖靜態內部實現類FairSync
。
如上文的原始碼介紹,這兩個類的的加鎖邏輯基本一致,唯一的區別在於:公平鎖實現類加鎖時,增加了一個hasQueuedPredecessors()
方法判斷,這個方法會判斷等待佇列是否有執行緒處於等待狀態,如果沒有,嘗試獲取鎖;如果有,就進入等待佇列。
簡單的說就是,非公平鎖實現類的加鎖方式,如果有執行緒嘗試獲取鎖,直接嘗試透過CAS
方式進行搶鎖,如果搶成功了,就直接獲取鎖,沒有搶成功就進入等待佇列;而公平鎖實現類的加鎖方式,會判斷等待佇列是否有執行緒處於等待狀態,如果有則不去搶鎖,乖乖排到後面,如果沒有則嘗試搶鎖。
相對來說,非公平鎖會有更好的效能,因為它的吞吐量比較大。其次,非公平鎖讓獲取鎖的時間變得更加不確定,可能會導致在阻塞佇列中的執行緒長期處於飢餓狀態。
Semaphore
類的公平鎖和非公平鎖實現也類似,擁有兩個靜態內部實現類,原始碼就不再解讀了,有興趣的朋友可以自行閱讀。
3.2.3、主要模板方法
從ReentrantLock
的原始碼實現中可以看出,AQS
使用了模板方法設計模式,它不提供加鎖和釋放鎖的具體邏輯實現,而是由實現類重寫對應的方法來完成,這樣的好處就是實現更加的靈活,不同的執行緒同步器可以自行繼承AQS
類,然後實現獨屬於自身的加鎖和解鎖功能。
常用的模板方法主要有以下幾個:
方法 | 描述 |
---|---|
protected boolean isHeldExclusively() | 判斷該執行緒是否正在獨佔資源。只有用到Condition 才需要去實現它 |
protected boolean tryAcquire(int arg) | 獨佔方式。嘗試獲取資源,arg 為獲取鎖的次數,成功則返回true ,失敗則返回false |
protected boolean tryRelease(int arg) | 獨佔方式。嘗試釋放資源,arg 為釋放鎖的次數,成功則返回true ,失敗則返回false |
protected int tryAcquireShared(int arg) | 共享方式。嘗試獲取資源,arg 為獲取鎖的次數,負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源 |
protected boolean tryReleaseShared(int arg) | 共享方式。嘗試釋放資源,arg 為釋放鎖的次數,如果釋放後允許喚醒後續等待結點返回true ,否則返回false |
通常自定義執行緒同步器,要麼是獨佔模式,要麼是共享模式。
如果是獨佔模式,重寫tryAcquire()
和tryRelease()
方法即可,比如 ReentrantLock 類。
如果是共享模式,重寫tryAcquireShared()
和tryReleaseShared()
方法即可,比如 Semaphore 類。
3.2.4、執行緒加入等待佇列實現
當執行緒呼叫tryAcquire()
方法獲取鎖失敗之後,就會呼叫addWaiter()
方法,將當前執行緒加入到等待佇列中去。
addWaiter()
方法,部分核心原始碼如下:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
// 將當前執行緒加入等待佇列
private Node addWaiter(Node mode) {
// 以當前執行緒構造一個節點,嘗試透過CAS方式插入到雙向連結串列的隊尾
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果插入沒有成功,則透過enq入隊
enq(node);
return node;
}
// 透過enq入隊
private Node enq(final Node node) {
// CAS 自旋方式,直到成功加入隊尾
for (;;) {
Node t = tail;
if (t == null) {
// 佇列為空,建立一個空結點作為head結點,並將tail也指向它
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
}
我們再來看看Node
類節點相關的屬性,部分核心原始碼如下:
static final class Node {
// 當前節點在佇列中的狀態,狀態值列舉含義如下:
// 0:節點初始化時的狀態
// 1: 表示節點引用執行緒由於等待超時或被打斷時的狀態
// -1: 表示當佇列中加入後繼節點被掛起時,其前驅節點會被設定為SIGNAL狀態,表示該節點需要被喚醒
// -2:當節點執行緒進入condition佇列時的狀態
// -3:僅在釋放共享鎖releaseShared時對頭節點使用
volatile int waitStatus;
// 前驅節點
volatile Node prev;
// 後繼節點
volatile Node next;
//該節點的執行緒例項
volatile Thread thread;
// 指向下一個處於Condition狀態的節點(用於條件佇列)
Node nextWaiter;
//...
}
可以很清晰的看到,每個關鍵屬性變數都加了volatile
修飾符,確保多執行緒環境下可見。
正如上文所介紹的,Node
其實是一個雙向連結串列資料結構,大致的資料結構圖如下!(圖片來自ReentrantLock 的實現看 AQS 的原理及應用 - 美團技術團隊)
其中第一個節點,也叫頭節點,為虛節點,並不儲存任何執行緒資訊,只是佔位用;真正有資料的是從第二個節點開始,當有執行緒需要加入等待佇列時,會向隊尾進行插入。
執行緒加入等待佇列之後,會再次呼叫acquireQueued()
方法,嘗試進行獲取鎖,如果成功或者中斷就退出,部分核心原始碼如下:
final boolean acquireQueued(final Node node, int arg) {
// 標記是否成功拿到鎖
boolean failed = true;
try {
// 標記等待過程中是否中斷過
boolean interrupted = false;
// 開始自旋,要麼獲取鎖,要麼中斷
for (;;) {
// 獲取當前節點的前驅節點
final Node p = node.predecessor();
// 如果p是頭結點,說明當前節點在等待佇列的頭部,嘗試獲取鎖(頭結點是虛節點)
if (p == head && tryAcquire(arg)) {
// 獲取鎖成功,頭指標移動到當前node
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果p不是頭節點或者是頭節點但獲取鎖失敗,判斷當前節點是否要進入阻塞,如果滿足要求,就透過park讓執行緒進入阻塞狀態,等待被喚醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 如果沒有成功獲取鎖(比如超時或者被中斷),那麼取消節點在佇列中的等待
cancelAcquire(node);
}
}
執行緒加入等待佇列實現,總結下來,大致步驟如下:
- 1.呼叫
addWaiter()
方法,將當前執行緒封裝成一個節點,嘗試透過CAS
方式插入到雙向連結串列的隊尾,如果沒有成功,再透過自旋方式插入,直到成功為止 - 2.呼叫
acquireQueued()
方法,對在等待佇列中排隊的執行緒,嘗試獲取鎖操作,如果失敗,判斷當前節點是否要進入阻塞,如果滿足要求,就透過LockSupport.park()
方法讓執行緒進入阻塞狀態,並檢查是否被中斷,如果沒有,等待被喚醒
3.2.5、執行緒從等待佇列中被喚醒實現
當執行緒呼叫tryRelease()
方法釋放鎖成功之後,會從等待佇列的頭部開始,獲取排隊的執行緒,並進行喚醒操作。
釋放鎖方法,部分核心原始碼如下:
public final boolean release(int arg) {
// 嘗試釋放鎖
if (tryRelease(arg)) {
// 獲取頭部節點
Node h = head;
if (h != null && h.waitStatus != 0)
// 嘗試喚醒頭部節點的下一個節點中的執行緒
unparkSuccessor(h);
return true;
}
return false;
}
其中unparkSuccessor()
是執行喚醒執行緒的核心方法,部分核心原始碼如下:
private void unparkSuccessor(Node node) {
// 獲取頭結點 waitStatus
int ws = node.waitStatus;
// 置零當前執行緒所在的結點狀態,允許失敗
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 獲取當前節點的下一個節點s
Node s = node.next;
// 如果下個節點是null或者被取消,就從佇列尾部依此尋找節點
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);
}
執行緒從等待佇列中被喚醒實現,總結下來,大致步驟如下:
- 1.當執行緒呼叫
tryRelease()
方法釋放鎖成功之後,會從等待佇列獲取排隊的執行緒 - 2.如果佇列的頭節點的下一個節點有效,會嘗試進行喚醒節點中的執行緒;如果為空或者被取消,就從佇列尾部依此尋找節點,找到佇列中排在最前的有效節點,並嘗試進行喚醒操作
四、簡單應用
瞭解完AQS
基本原理之後,按照以上的知識點,我們可以自己實現一個不可重入的互斥鎖執行緒同步類。
示例程式碼如下:
public class MutexLock {
// 自定義同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 判斷是否鎖定狀態
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 嘗試獲取資源,立即返回。成功則返回true,否則false。
@Override
protected boolean tryAcquire(int acquires) {
//state為0才設定為1,不支援重入!
if (compareAndSetState(0, 1)) {
//設定為當前執行緒獨佔資源
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 嘗試釋放資源,立即返回。成功則為true,否則false。
@Override
protected boolean tryRelease(int releases) {
// 判斷資源是否已被釋放
if (getState() == 0)
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
//釋放資源,放棄佔有狀態
setState(0);
return true;
}
}
// 真正同步類的實現都依賴繼承於AQS的自定義同步器!
private final Sync sync = new Sync();
// 獲取鎖,會阻塞等待,直到成功才返回
public void lock() {
sync.acquire(1);
}
// 釋放鎖
public void unlock() {
sync.release(1);
}
}
測試類如下:
public class MutexLockTest {
private static int count =0;
private static MutexLock lock = new MutexLock();
public static void main(String[] args) throws Exception {
final int threadNum = 10;
CountDownLatch latch = new CountDownLatch(threadNum);
// 建立10個執行緒,同時對count進行1000相加操作
for (int i = 0; i < threadNum; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// 加鎖
lock.lock();
for (int j = 0; j < 1000; j++) {
count++;
}
// 釋放鎖
lock.unlock();
// 執行緒數減 1
latch.countDown();
}
}).start();
}
// 等待執行緒執行完畢
latch.await();
System.out.println("執行結果:" + count);
}
}
輸出結果:
執行結果:10000
從日誌輸出結果可以清晰的看到,執行結果與預期值一致!
五、小結
本文從ReentrantLock
原始碼分析到AQS
原理解析,進行了一次知識內容的總結,從上文的分析中可以看出,AQS
是JUC
包下執行緒同步器實現的基石。
ReentrantLock、ReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore、ThreadPoolExecutor 等併發工具類,執行緒同步的實現都基於AQS
來完成,掌握AQS
原理對執行緒同步的理解和使用至關重要。
AQS
原理是面試時熱點話題,希望本篇能幫助到大家!
六、參考
1.https://www.cnblogs.com/waterystone/p/4920797.html
2.https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
3.https://zhuanlan.zhihu.com/p/197840259
4.https://juejin.cn/post/7006895386103119908