深入淺出Java多線程(十一):AQS

来源:https://www.cnblogs.com/CoderLvJie/p/18068149
-Advertisement-
Play Games

大家好,我是你們的老伙計秀才!今天帶來的是[深入淺出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代表一個等待線程,並通過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()方法釋放鎖:

	   

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近項目中有一個需要使用QT生成固定長度隨機字元串的需求,需求也很簡單,就是生成一個n位的僅包含0-9以及大寫字母的字元串,因為這也是第一次使用QT自身的隨機數,這裡就做一下簡單記錄。 廢話不多說,直接上代碼。 1 QString getRandomString(int length) 2 { 3 ...
  • 8.1 C++內聯函數 提出的目的:為了提高程式運行速度。 內聯函數和普通函數的區別: 編譯方式: 內聯函數在編譯時會被直接替換到調用處,而不是像普通函數那樣通過函數調用的方式執行。這樣可以減少函數調用的開銷,提高程式執行效率。 普通函數則是通過函數調用的方式執行,會涉及函數棧的壓棧和出棧操作。 代 ...
  • Java Iterator Iterator 介面提供了一種迭代集合的方法,即順序訪問集合中的每個元素。它支持 hasNext() 和 next() 方法,用於檢查是否存在下一個元素以及獲取下一個元素。 獲取 Iterator 可以使用集合的 iterator() 方法獲取 Iterator 實例: ...
  • 摘要: 銀行卡歸屬地查詢介面是一種高效的方式,通過銀行卡號查詢銀行名稱、卡種、卡品牌以及發卡省份和城市等信息。本文將詳細介紹如何使用該介面,並附帶代碼說明。同時,也介紹了介面的特點和適用範圍,讓讀者能夠充分瞭解和運用該介面,方便快捷地獲取銀行卡發卡行所在地信息。 一、介面簡介 銀行卡歸屬地查詢介面是 ...
  • 1. 本篇文章目標 將下麵的excel中的寄存器表單讀入並構建一個字典 2. openpyxl的各種基本使用方法 2.1 打開工作簿 wb = openpyxl.load_workbook('test_workbook.xlsx') 2.2 獲取工作簿中工作表名字並得到工作表 ws = wb[wb. ...
  • 拓展閱讀 linux Shell 命令行-00-intro 入門介紹 linux Shell 命令行-02-var 變數 linux Shell 命令行-03-array 數組 linux Shell 命令行-04-operator 操作符 linux Shell 命令行-05-test 驗證是否符 ...
  • 在之前的多線程系列文章中,我們陸陸續續的介紹了Thread線程類相關的知識和用法,其實在Thread類上還有一層ThreadGroup類,也就是線程組。 ...
  • 2024年3月4日,官方宣佈推出 Claude 3 模型系列,它在廣泛的認知任務中樹立了新的行業基準。該系列包括三個按能力遞增排序的最先進模型:Claude 3 Haiku、Claude 3 Sonnet 和 Claude 3 Opus。每個後續模型都提供越來越強大的性能,允許用戶為其特定應用選擇智 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...