深入淺出Java多執行緒(十一):AQS

解码猿發表於2024-03-12

引言


大家好,我是你們的老夥計秀才!今天帶來的是[深入淺出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代表一個等待執行緒,並透過prevnext指標形成連結串列結構。頭尾指標headtail分別指向佇列的首尾結點,便於進行快速插入和移除操作。

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節點的設計上,透過SHAREDEXCLUSIVE靜態常量區分不同模式的節點。

儘管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()方法釋放鎖:

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void criticalSection() {
        lock.lock();
        try {
            // 執行臨界區程式碼
        } finally {
            lock.unlock(); // 釋放鎖,可能喚醒等待佇列中的執行緒
        }
    }
}

在這個例子中,當執行到finally塊的unlock()方法時,就觸發了AQS內部的資源釋放邏輯,從而有可能喚醒另一個之前因無法獲取鎖而進入等待狀態的執行緒。

總結


AbstractQueuedSynchronizer(AQS)作為Java併發程式設計中至關重要的框架,為構建高效、安全的鎖和其他同步器提供了基礎結構。它巧妙地結合了資料結構和原子操作,實現了執行緒間的資源共享管理,並支援獨佔模式和共享模式兩種主要的同步方式。

在AQS的設計中,volatile變數state是資源狀態的核心表示,透過tryAcquire(int)tryRelease(int)等protected方法,子類可以靈活定義資源獲取和釋放的具體邏輯。同時,AQS利用FIFO雙端佇列和Node節點結構來維護等待獲取資源的執行緒佇列,確保了執行緒間的公平性和互斥性。

對於資源的獲取流程,AQS採用自旋+CAS的方式插入新的等待節點至隊尾,當無法立即獲取資源時,執行緒會進入等待狀態並透過LockSupport.park阻塞自身。而在資源釋放時,AQS則透過unparkSuccessor方法喚醒等待佇列中的下一個合適節點,使得資源能夠被有效地傳遞給其他執行緒。

例如,在ReentrantLock中,AQS用於實現可重入的鎖功能,當執行緒呼叫lock()方法嘗試獲取鎖時,如果當前鎖已被佔用,則執行緒將加入等待佇列;而當執行緒呼叫unlock()方法釋放鎖時,AQS會自動處理後續執行緒的喚醒工作。

總的來說,AQS透過模板方法設計模式,簡化了自定義同步元件的開發難度,開發者僅需關注資源訪問策略的實現,即可構建出如ReentrantLock、Semaphore、CountDownLatch等多種廣泛應用的同步工具類。AQS以其強大的核心機制,極大地提升了Java多執行緒環境下的同步效能和靈活性,成為併發程式設計庫不可或缺的基石。

相關文章