大家好,我是你們的老伙計秀才!今天帶來的是[深入淺出Java多線程]系列的第十一篇內容:AQS(*AbstractQueuedSynchronizer*)。大家覺得有用請點贊,喜歡請關註!秀才在此謝過大家了!!! ...
引言
大家好,我是你們的老伙計秀才!今天帶來的是[深入淺出Java多線程]系列的第十一篇內容:AQS(AbstractQueuedSynchronizer)。大家覺得有用請點贊,喜歡請關註!秀才在此謝過大家了!!!
在現代多核CPU環境中,多線程編程已成為提升系統性能和併發處理能力的關鍵手段。然而,當多個線程共用同一資源或訪問臨界區時,如何有效地控制線程間的執行順序以保證數據一致性及避免競態條件變得至關重要。Java平臺為解決這些問題提供了多種同步機制,如synchronized關鍵字、volatile變數以及更加靈活且功能強大的併發工具類庫——java.util.concurrent包。
在這一龐大的併發工具箱中,AbstractQueuedSynchronizer(簡稱AQS)扮演了核心角色。作為Java併發框架中的基石,AQS是一個高度抽象的底層同步器,它不僅被廣泛應用於諸如ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等標準同步組件,還為開發者提供了一種便捷的方式來構建符合特定需求的自定義同步器。
AQS的設計理念是基於模板方法模式,通過封裝複雜的同步狀態管理和線程排隊邏輯,使得子類只需關註並實現資源獲取與釋放的核心演算法即可。它使用一個名為state
的volatile變數來表示同步狀態,並藉助於FIFO雙端隊列結構來管理等待獲取資源的線程。AQS內部維護的Node節點不僅包含了每個等待線程的信息,而且還通過waitStatus標誌位巧妙地實現了獨占式和共用式的兩種資源共用模式。
例如,在ReentrantLock中,AQS負責記錄當前持有鎖的線程重入次數,而當線程嘗試獲取但無法立即獲得鎖時,會將該線程包裝成Node節點並安全地插入到等待隊列中。隨後,線程會被優雅地阻塞,直至鎖被釋放或者其在等待隊列中的位置變為可以獲取資源的狀態。這個過程涉及到一系列精心設計的方法調用,如tryAcquire(int)、acquireQueued(Node, int)和release(int)等。
// 示例代碼:ReentrantLock基於AQS的簡單應用
import java.util.concurrent.locks.ReentrantLock;
public class AQSExample {
private final ReentrantLock lock = new ReentrantLock();
public void criticalSection() {
lock.lock(); // 調用lock()即嘗試獲取AQS的資源
try {
// 臨界區代碼
System.out.println("Thread " + Thread.currentThread().getName() + " is executing critical section.");
} finally {
lock.unlock(); // 釋放資源
}
}
public static void main(String[] args) {
AQSExample example = new AQSExample();
Thread t1 = new Thread(example::criticalSection, "Thread-1");
Thread t2 = new Thread(example::criticalSection, "Thread-2");
t1.start();
t2.start();
}
}
在這個簡單的示例中,我們創建了一個ReentrantLock實例併在兩個線程中分別調用lock方法進入臨界區。如果第一個線程已經占有鎖,第二個線程將會進入等待隊列,直到鎖被釋放。這背後的機制正是由AQS提供的強大同步支持所驅動的。通過對AQS的深入探討,讀者將能更好地理解這些高級同步工具的內部工作原理,從而更高效地進行併發編程實踐。
AQS簡介
在Java多線程編程中,AbstractQueuedSynchronizer(簡稱AQS)作為J.U.C包下的一款核心同步框架,扮演了構建高效併發鎖和同步器的重要角色。AQS的設計理念與實現機制極大地簡化了開發人員創建自定義同步組件的工作量,同時提供了強大的底層支持以滿足多樣化的併發控制需求。
隊列管理: 從數據結構層面看,AQS內部維護了一個基於先進先出(FIFO)原則的雙端隊列。該隊列並非直接存儲線程對象,而是使用Node節點表示等待資源的線程,並通過volatile變數state記錄當前資源的狀態。AQS利用兩個指針head和tail精確地跟蹤隊列的首尾位置,確保線程在無法立即獲取資源時能夠安全且有序地進入等待狀態。
同步功能: AQS不僅實現了對資源的原子操作,例如通過getState()
、setState()
以及基於Unsafe的compareAndSetState()
方法保證資源狀態更新的原子性和可見性,還提供了線程排隊和阻塞機制,包括線程等待隊列的維護、入隊與出隊的邏輯,以及線程在資源未得到時如何正確地掛起和喚醒等核心功能。
應用實例: AQS的強大之處在於它支撐了許多常見的併發工具類,諸如ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock以及SynchronousQueue等,這些同步工具均是建立在AQS基礎之上的,有效地解決了多線程環境下的互斥訪問、信號量控制、倒計數等待、讀寫分離等多種同步問題。
下麵是一個簡單的代碼示例,展示瞭如何使用基於AQS實現的ReentrantLock進行線程同步:
import java.util.concurrent.locks.ReentrantLock;
public class AQSExample {
private final ReentrantLock lock = new ReentrantLock();
public void criticalSection() {
lock.lock(); // 調用lock()方法嘗試獲取AQS管理的資源
try {
// 執行臨界區代碼
System.out.println("Thread " + Thread.currentThread().getName() + " is in the critical section.");
} finally {
lock.unlock(); // 在finally塊中確保資源始終會被釋放
}
}
public static void main(String[] args) {
AQSExample example = new AQSExample();
Thread t1 = new Thread(example::criticalSection, "Thread-1");
Thread t2 = new Thread(example::criticalSection, "Thread-2");
t1.start();
t2.start();
}
}
在這個例子中,當一個線程調用lock方法併成功獲取到資源(即獲得鎖)時,另一個線程必須等待直至鎖被釋放。這一過程正是通過AQS所維護的線程等待隊列和相應的同步演算法得以實現的。此外,AQS也支持資源共用的兩種模式,即獨占模式(一次只有一個線程能獲取資源)和共用模式(允許多個線程同時獲取資源但數量有限制),並且靈活地支持可中斷的資源請求操作,為複雜多樣的併發場景提供了一站式的解決方案。
AQS的數據結構
在Java多線程編程中,AbstractQueuedSynchronizer(AQS)的數據結構設計是其高效實現同步功能的關鍵。AQS的核心數據結構主要包括以下幾個部分:
volatile變數state:
AQS內部維護了一個名為state
的volatile整型變數,用於表示共用資源的狀態。該狀態值可以用來反映資源的數量、鎖的持有狀態等信息,具體含義由基於AQS構建的具體同步組件定義。由於state是volatile修飾的,因此確保了對它的修改能被其他線程及時看到,實現了跨線程的記憶體可見性。
protected volatile int state;
Node雙端隊列:
AQS使用一個FIFO(先進先出)的雙端隊列來存儲等待獲取資源的線程。這裡的節點並非直接存儲線程對象,而是封裝為Node
類的對象,每個Node代表一個等待線程,並通過prev
和next
指針形成鏈表結構。頭尾指針head
和tail
分別指向隊列的首尾結點,便於進行快速插入和移除操作。
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
// 其他成員方法及屬性...
}
waitStatus標誌位:
每個Node節點都有一個waitStatus
欄位,它是一個int類型的volatile變數,用以標識當前節點所對應的線程等待狀態。例如,CANCELLED
表示線程已經被取消,SIGNAL
表示後繼節點的線程需要被喚醒,CONDITION
則表示線程在條件隊列中等待某個條件滿足,還有如PROPAGATE
這樣的狀態值用於共用模式下的資源傳播。
線程調度邏輯: 當線程嘗試獲取資源失敗時,會創建一個Node節點並將當前線程包裝進去,然後利用CAS演算法將其安全地加入到等待隊列的尾部。而在釋放資源時,AQS會根據資源管理策略從隊列中選擇合適的節點並喚醒對應線程。
資源共用模式支持:
AQS內建了對獨占模式和共用模式的支持,這兩種模式的區別在於:獨占模式下同一時刻只能有一個線程獲取資源,典型的如ReentrantLock;而共用模式允許多個線程同時獲取資源,如Semaphore和CountDownLatch。在Node節點的設計上,通過SHARED
和EXCLUSIVE
靜態常量區分不同模式的節點。
儘管AQS提供瞭如tryAcquire(int)
、tryRelease(int)
等方法供子類覆蓋以完成特定的資源控制邏輯,但具體的線程入隊與出隊、狀態更新以及阻塞與喚醒等底層細節都是由AQS本身精心設計並實現的。這種機制使得基於AQS構建的同步工具能夠有效地處理併發場景中的競爭問題,保證了線程間的安全協同執行。遺憾的是,由於篇幅限制,在此處無法提供完整的代碼示例來展示AQS如何將線程包裝成Node節點並維護其線上程等待隊列中的位置變化。
總結AQS的數據結構如下圖:
資源共用模式
在Java多線程同步框架AbstractQueuedSynchronizer(AQS)中,資源共用模式是其核心概念之一,用於定義併發環境中資源的訪問方式。AQS支持兩種主要的資源共用模式:獨占模式(Exclusive)和共用模式(Share)。
獨占模式:
在獨占模式下,同一時間只能有一個線程獲取並持有資源,典型的例子就是ReentrantLock。當一個線程成功獲取鎖之後,其他試圖獲取鎖的線程將被阻塞,直到持有鎖的線程釋放資源。通過AQS中的tryAcquire(int)
方法實現對資源的嘗試獲取,以及tryRelease(int)
方法來釋放資源。例如:
import java.util.concurrent.locks.ReentrantLock;
public class ExclusiveModeExample {
private final ReentrantLock lock = new ReentrantLock();
public void criticalSection() {
lock.lock(); // 嘗試以獨占模式獲取資源(即獲取鎖)
try {
// 在這裡執行臨界區代碼
} finally {
lock.unlock(); // 釋放資源(即釋放鎖)
}
}
public static void main(String[] args) {
ExclusiveModeExample example = new ExclusiveModeExample();
Thread t1 = new Thread(example::criticalSection, "Thread-1");
Thread t2 = new Thread(example::criticalSection, "Thread-2");
t1.start();
t2.start();
}
}
在這個示例中,兩個線程嘗試進入臨界區,但由於使用的是ReentrantLock(基於AQS),因此在同一時刻僅允許一個線程執行臨界區代碼。
共用模式: 而在共用模式下,多個線程可以同時獲取資源,但通常會限制可同時訪問資源的線程數量。Semaphore和CountDownLatch就是採用共用模式的例子。例如,在Semaphore中,可以通過參數指定允許多少個線程同時訪問某個資源:
import java.util.concurrent.Semaphore;
public class SharedModeExample {
private final Semaphore semaphore = new Semaphore(3); // 只允許最多3個線程同時訪問資源
public void accessResource() {
try {
semaphore.acquire(); // 獲取許可,如果當前可用許可數小於1,則線程會被阻塞
// 在這裡執行需要保護的共用資源操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 釋放許可,使其他等待的線程有機會繼續訪問
}
}
public static void main(String[] args) {
SharedModeExample example = new SharedModeExample();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(example::accessResource, "Thread-" + (i + 1));
t.start();
}
}
}
此例中,Semaphore初始化為3個許可,這意味著最多三個線程可以同時執行accessResource
方法中的共用資源操作。超過三個線程則需等待其他線程釋放許可後才能繼續執行。
總之,無論是獨占模式還是共用模式,AQS都提供了底層機制來確保線程安全地進行資源的獲取與釋放,並利用雙端隊列結構及狀態變數維護線程的等待、喚醒邏輯,使得這些高級同步工具能夠在各種複雜的併發場景中表現得既高效又穩定。
AQS關鍵方法解析
在Java多線程同步框架AbstractQueuedSynchronizer(AQS)中,有幾個關鍵方法是實現資源獲取與釋放的核心邏輯。這些方法由子類覆蓋以滿足特定的同步需求,並結合AQS提供的底層隊列管理和狀態更新機制,確保了線程間的同步操作正確且高效地執行。
tryAcquire(int arg) 和 tryRelease(int arg): 這兩個方法分別對應資源的獨占式獲取和釋放操作。在ReentrantLock等基於AQS構建的獨占鎖中,子類需要重寫這兩個方法來定義資源是否可以被當前線程獲取或釋放的條件。例如,在ReentrantLock中,tryAcquire會檢查當前線程是否已經持有鎖以及鎖的狀態是否允許重新獲取;tryRelease則負責遞減鎖的計數並根據結果決定是否喚醒等待隊列中的線程。
tryAcquireShared(int arg) 和 tryReleaseShared(int arg): 對於共用模式下的資源控制,AQS提供了這兩個方法。在Semaphore、CountDownLatch等共用資源管理器中,tryAcquireShared將嘗試獲取指定數量的資源,並返回一個表示成功與否及剩餘資源量的整數值;而tryReleaseShared則是釋放資源,同樣根據資源總量的變化判斷是否有等待的線程可以被喚醒。
acquire(int arg) 和 release(int arg): 這是AQS對外暴露的主要介面,用於資源的獲取和釋放。acquire首先調用tryAcquire試圖直接獲取資源,若失敗則通過addWaiter方法將當前線程包裝成Node節點加入到等待隊列尾部,併進一步調用acquireQueued進入自旋迴圈直至成功獲取資源或被中斷。acquireQueued內部包含parkAndCheckInterrupt方法,使用LockSupport.park掛起當前線程,直到其他線程釋放資源後通過unpark喚醒它。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
acquireInterruptibly(int arg) 和 acquireSharedInterruptibly(int arg): 這兩個方法擴展了acquire和acquireShared的功能,使其支持可中斷的資源請求。如果在等待過程中線程被中斷,將會拋出InterruptedException,而非一直阻塞。
isHeldExclusively(): 這個方法僅在使用條件變數時有用,用於確定當前線程是否獨占資源。在ReentrantLock的Condition實現中,該方法用於檢測當前線程是否持有鎖,以便決定能否執行signal/signalAll等操作。
綜上所述,AQS通過提供一套模板方法供子類擴展,從而實現了靈活且高效的線程同步機制。在實際應用中,開發者可以根據具體場景重寫相應的tryAcquire系列方法,利用AQS強大的底層隊列和原子狀態管理功能來實現複雜的併發控制邏輯。
總結AQS的流程如下圖:
AQS資源釋放
在Java多線程同步框架AbstractQueuedSynchronizer(AQS)中,資源釋放邏輯是同步機制中的重要一環。當一個線程完成了對共用資源的獨占或共用操作後,需要通過調用相應的release方法來釋放資源,使得等待隊列中的其他線程有機會獲取並使用這些資源。
資源釋放入口:
資源釋放的主要入口是release(int arg)
方法,它接受一個參數arg,表示要釋放的資源數量。此方法首先調用子類實現的tryRelease(int arg)
方法嘗試釋放資源。如果該方法返回true,說明資源成功釋放,此時AQS會進一步檢查當前頭節點的狀態,並決定是否喚醒下一個等待的線程。
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(Node node)
方法會被調用來喚醒等待隊列中合適的下一個線程。這個方法首先檢查頭結點的waitStatus狀態,如果大於等於0,則遍歷隊列以找到首個可用的未取消結點,並使用LockSupport.unpark喚醒對應的線程。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, 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;
}
if (s != null)
LockSupport.unpark(s.thread);
}
中斷與資源管理: 對於支持可中斷的同步器如ReentrantLock,其釋放資源的過程還會考慮線程中斷的情況。當一個線程在等待過程中被中斷時,它的等待狀態將被正確處理,並可能拋出InterruptedException異常,從而允許上層代碼進行恰當的響應。
此外,在資源釋放的過程中,AQS確保了操作的原子性和一致性,防止多個線程同時釋放資源造成混亂。正是由於這種精心設計的資源釋放邏輯,基於AQS構建的同步組件才能夠高效、安全地協調多線程對共用資源的訪問。
舉例來說,在使用ReentrantLock時,線程在完成臨界區代碼後應調用lock對象的unlock()方法釋放鎖: