AQS簡介
AQS全稱AbstractQueuedSynchronizer,抽象隊列同步器,是一個實現同步組件的基礎框架。AQS使用一個int類型的成員變量state維護同步狀態,通過內置的同步隊列(CLH鎖、FIFO)完成線程的排隊工作,底層主要是通過CAS操作和volatile特性實現。
它主要包含兩種模式:獨占模式(例如ReentrantLock)和共享模式(例如CountDownLatch)。關鍵方法包括acquire和release,用于獨占模式下的獲取和釋放操作,以及acquireShared和releaseShared,用于共享模式下的操作。
它簡化了同步組件的實現方式,屏蔽了同步狀態的管理、線程的排隊等待與喚醒等底層操作。我們只需要通過繼承實現AQS中的抽象方法,即可實現不同同步語義的同步組件。例如ReentrantLock這種獨占式同步組件,核心邏輯僅重寫了tryAcquire和tryRelease等少數方法,就實現了可重入獨占式鎖的功能。
AQS通過繼承來擴展其功能,子類通過繼承AQS實現抽象方法來管理同步狀態,同步組件則通過聚合子類的方法來實現自己的同步特性,獨占式或共享式。
使用AQS實現一個自定義排他鎖
繼承AQS:
創建一個新的類,繼承AbstractQueuedSynchronizer。
實現核心方法:
實現tryAcquire和tryRelease方法(用于獨占模式),或實現tryAcquireShared和tryReleaseShared方法(用于共享模式)。
內部類Sync:
通常創建一個內部靜態類Sync,擴展AQS并實現上述方法。
對外方法:
在外部類中,提供獲取和釋放的方法,內部調用AQS的方法acquire、release、acquireShared和releaseShared。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;public class SimpleLock {private final Sync sync = new Sync();private static class Sync extends AbstractQueuedSynchronizer {@Overrideprotected boolean tryAcquire(int arg) {if (compareAndSetState(0, 1)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}@Overrideprotected boolean tryRelease(int arg) {if (getState() == 0) throw new IllegalMonitorStateException();setExclusiveOwnerThread(null);setState(0);return true;}@Overrideprotected boolean isHeldExclusively() {return getExclusiveOwnerThread() == Thread.currentThread();}}public void lock() {sync.acquire(1);}public void unlock() {sync.release(1);}public boolean isLocked() {return sync.isHeldExclusively();}
}
使用AQS實現一個自定義共享鎖
import java.util.concurrent.locks.AbstractQueuedSynchronizer;public class SimpleSemaphore {private final Sync sync;public SimpleSemaphore(int permits) {sync = new Sync(permits);}private static class Sync extends AbstractQueuedSynchronizer {Sync(int permits) {setState(permits);}@Overrideprotected int tryAcquireShared(int acquires) {for (;;) {int available = getState();int remaining = available - acquires;if (remaining < 0 || compareAndSetState(available, remaining)) {return remaining;}}}@Overrideprotected boolean tryReleaseShared(int releases) {for (;;) {int current = getState();int next = current + releases;if (compareAndSetState(current, next)) {return true;}}}}public void acquire() throws InterruptedException {sync.acquireSharedInterruptibly(1);}public void release() {sync.releaseShared(1);}
}
同步隊列和條件隊列的區別
同步隊列(synchronization queue)和條件隊列(condition queue)是AbstractQueuedSynchronizer(AQS)中的兩個不同的隊列,它們有不同的用途和工作機制。
同步隊列
- 用途:管理所有等待獲取鎖或同步狀態的線程。
- 觸發條件:當線程嘗試獲取鎖或同步狀態失敗時進入隊列。
- 結構:雙向鏈表。
- 作用:維護線程的順序,確保公平競爭。
- 喚醒機制:當鎖釋放或同步狀態改變時,喚醒隊列中的下一個線程。
條件隊列
- 用途:管理等待特定條件的線程(通過ConditionObject)。
- 觸發條件:當線程調用await()方法時進入隊列,并釋放當前持有的鎖。
- 結構:單向鏈表。
- 作用:等待特定條件的滿足,如某個狀態的變化。
- 喚醒機制:當條件滿足時(通過signal()或signalAll()),線程從條件隊列移動到同步隊列,等待重新獲取鎖。
為什么await方法一定要加鎖才能調用
我理解條件隊列現在的實現是在await方法先完全釋放鎖,再進入休眠狀態等待喚醒,被喚醒后重新嘗試加鎖。先完全釋放鎖,是為了防止當前線程持有鎖時錯誤休眠,若當前線程持有鎖進入休眠狀態,即使當前線程被喚醒,也是嘗試加鎖而不是解鎖,那么其他線程永遠無法拿到鎖。因此先完全解鎖再休眠,被喚醒后重新嘗試加鎖,應該是出于防止鎖饑餓與死鎖的考慮。
AQS獨占式加鎖(以ReentrantLock加鎖為例)
加鎖流程
加鎖源碼分析
解鎖流程
解鎖源碼分析
CLH鎖(同步隊列的原型)
CLH鎖是對自旋鎖的一種改良,自旋鎖只適合競爭不激烈,加鎖時間短的場景,原因是自旋鎖有2個問題,第一,存在鎖饑餓問題,可能會出現某個線程一直拿不到鎖的情況;第二,鎖狀態中心化,激烈競爭時會導致CPU的高速緩存頻繁失效,導致性能降低。
CLH隊列鎖有效地解決了以上2個問題,首先,CLH鎖通過增加隊列進行排隊,確保所有線程都有機會拿到鎖;第二,CLH鎖的每個線程監視的都是上一個節點的鎖狀態,因此鎖狀態是去中心化的,不會導致CPU高速緩存頻繁失效。
CLH鎖實現源碼
import java.util.concurrent.atomic.AtomicReference;public class CLH {private final ThreadLocal<Node> node = ThreadLocal.withInitial(Node::new);private final AtomicReference<Node> tail = new AtomicReference<>(new Node());private static class Node {// ①鎖定狀態必須用volatile修飾以確保內存可見性private volatile boolean locked;}public void lock() {Node node = this.node.get();node.locked = true;Node pre = this.tail.getAndSet(node);while (pre.locked) ;}public void unlock() {Node node = this.node.get();node.locked = false;// ②重置當前線程對應的Node節點避免死鎖this.node.set(new Node());}
}
原理分析
觀察代碼可以發現,CLH沒有前驅或后繼指針,因為CLH鎖是一種隱式隊列,入隊只需要添加新的tail節點,出隊只需要修改頭部狀態。注釋①處必須用volatile修飾,以確保內存可見性與代碼有序性。注釋②處必須重置線程Node,否則當一個線程重復加鎖時,就有可能死鎖,死鎖原因可以看這篇文章的解鎖分析部分。
CLH鎖的優點是性能優異、實現簡單、加鎖公平、擴展性強。但缺點是有自旋操作,長時間加鎖會浪費CPU性能,再就是功能單一,不支持復雜的功能。
針對以上兩個缺點,AQS都做了改進。針對第一個缺點,將自旋改為了線程阻塞等待;針對第二個缺點,AQS做了很多改造和擴展,例如增加了每個節點的狀態waitStatus、顯式維護了前驅和后繼節點等等,基于這些擴展,AQS實現了獨占鎖、共享鎖、線程排隊、狀態傳播等復雜功能。
參考鏈接
從ReentrantLock的實現看AQS的原理及應用 - 美團技術團隊
AQS 詳解 | JavaGuide
AQS的前菜—詳解CLH隊列鎖_clh鎖-CSDN博客
Java AQS 核心數據結構-CLH 鎖
JUC鎖:核心類AQS源碼詳解 - 拿了桔子跑-范德依彪 - 博客園