AQS、CAS詳解

AQS詳解

原子性操作自:原子性在一個操作是不可中斷的,要麼全部執行成功要麼全部執行失敗,有着“同生共死”的感覺。及時在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程所幹擾2113

CAS:

全稱(Compare And Swap),比較交換,Unsafe類是CAS的核心類,提供硬件級別的原子操作

CAS 中有三個參數:內存值 V、舊的預期值 E、要更新的值 N ,當且僅當內存值 V 的值等於舊的預期值 A 時,纔會將內存值V的值修改爲 B ,否則什麼都不幹。

CAS比較與交換的僞代碼可以表示爲:

do{
		備份舊數據;
		基於舊數據構造新數據;
}while(!CAS( 內存地址,備份的舊數據,新數據 ))

在這裏插入圖片描述

但是存在這樣一種情況:如果一個值原來是A,變成了B,然後又變成了A,那麼在CAS檢查的時候會發現沒有改變,但是實質上它已經發生了改變,這就是所謂的ABA問題。對於ABA問題其解決方案是加上版本號,即在每個變量都加上一個版本號,每次改變時加1,即A —> B —> A,變成1A —> 2B —> 3A。
ABA問題導致的原因,是CAS過程中只簡單進行了“值”的校驗,再有些情況下,“值”相同不會引入錯誤的業務邏輯(例如庫存),有些情況下,“值”雖然相同,卻已經不是原來的數據了。

AQS:

AQS:AbstractQuenedSynchronizer抽象的隊列式同步器。是除了java自帶的synchronized關鍵字之外的鎖機制,它提供了一種實現阻塞鎖和一系列依賴FIFO等待隊列的同步器的框架。

AQS的核心思想是,如果被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並將共享資源設置爲鎖定狀態,如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列(FIFO)鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。

​ ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等併發類均是基於AQS來實現的,具體用法是通過繼承AQS實現其模板方法,然後將子類作爲同步組件的內部類。

同步狀態:

AQS 的主要使用方式是繼承,子類通過繼承同步器,並實現它的抽象方法來管理同步狀態。

AQS 使用一個 int 類型的成員變量 state表示同步狀態

  • state > 0 時,表示已經獲取了鎖。
  • state = 0 時,表示釋放了鎖。

它提供了三個方法,來對同步狀態 state 進行操作,並且 AQS 可以確保對 state 的操作是安全的:

  • #getState()
  • #setState(int newState)
  • #compareAndSetState(int expect, int update)

鎖的獨佔與共享:

​ 獨佔鎖:每次只能有一個線程能持有鎖,ReentrantLock就是以獨佔方式實現的互斥鎖。
​ 共享鎖:允許多個線程同時獲取鎖,併發訪問。

​ AQS提供了獨佔鎖和共享鎖必須實現的方法,具有獨佔鎖功能的子類,它必須實現tryAcquire、tryRelease、isHeldExclusively等;共享鎖功能的子類,必須實現tryAcquireShared和tryReleaseShared等方法,帶有Shared後綴的方法都是支持共享鎖加鎖的語義。

獨佔鎖獲取鎖時,設置節點模式爲Node.EXCLUSIVE

​ 獨佔式獲取同步狀態。如果當前線程獲取同步狀態成功,則由該方法返回;否則,將會進入同步隊列等待。該方法將會調用可重寫#tryAcquire(int arg) 方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

#acquireInterruptibly(int arg):與 #acquire(int arg) 相同,但是該方法響應中斷。當前線程爲獲取到同步狀態而進入到同步隊列中,如果當前線程被中斷,則該方法會拋出InterruptedException 異常並返回。

共享鎖獲取鎖,節點模式則爲Node.SHARED:
private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

主要內置方法:

  1. #tryAcquire(int arg):獨佔式獲取同步狀態,獲取同步狀態成功後,其他線程需要等待該線程釋放同步狀態才能獲取同步狀態。
  2. #tryRelease(int arg):獨佔式釋放同步狀態。
  3. #tryAcquireShared(int arg):共享式獲取同步狀態,返回值大於等於 0 ,則表示獲取成功;否則,獲取失敗。
  4. #tryReleaseShared(int arg):共享式釋放同步狀態。
  5. #isHeldExclusively():當前同步器是否在獨佔式模式下被線程佔用,一般該方法表示是否被當前線程所獨佔。
  6. #tryAcquireNanos(int arg, long nanos):超時獲取同步狀態。如果當前線程在 nanos 時間內沒有獲取到同步狀態,那麼將會返回 false ,已經獲取則返回 true 。
  7. #acquireShared(int arg):共享式獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待,與獨佔式的主要區別是在同一時刻可以有多個線程獲取到同步狀態;
  8. #acquireSharedInterruptibly(int arg):共享式獲取同步狀態,響應中斷。
  9. #tryAcquireSharedNanos(int arg, long nanosTimeout):共享式獲取同步狀態,增加超時限制。
  10. #release(int arg):獨佔式釋放同步狀態,該方法會在釋放同步狀態之後,將同步隊列中第一個節點包含的線程喚醒。
  11. #releaseShared(int arg):共享式釋放同步狀態。

CLH同步隊列:

AQS 通過內置的 FIFO 同步隊列來完成資源獲取線程的排隊工作

  • 如果當前線程獲取同步狀態失敗(鎖)時,AQS 則會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程
  • 當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。

在這裏插入圖片描述

​ CLH阻塞隊列採用的是雙向鏈表隊列,頭部節點默認獲取資源獲得執行權限。後續節點不斷自旋方式查詢前置節點是否執行完成,直到頭部節點執行完成將自己的waitStatus狀態修改以通知後續節點可以獲取資源執行。

AQS中Node節點:

static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
		//當該線程等待超時或者被中斷,需要從同步隊列中取消等待,則該線程被置1,即被取消
  	//節點進入了取消狀態則不再變化
    static final int CANCELLED =  1;
		//後繼的節點處於等待狀態,當前節點的線程如果釋放了同步狀態或者被取消(當前節點狀態置爲-1)
  	//只要前繼結點釋放鎖,就會通知標識爲SIGNAL狀態的後繼結點的線程執行
    static final int SIGNAL    = -1;
  	//該節點從等待隊列中轉移到同步隊列中,加入到對同步狀態的獲取中
    static final int CONDITION = -2;
		//該狀態標識結點的線程處於可運行狀態
    static final int PROPAGATE = -3;
		
    volatile int waitStatus;
		//前驅節點,當節點加入同步隊列的時候被設置(尾部添加)
    volatile Node prev;
		//後繼節點
    volatile Node next;
		//獲取同步狀態的線程
    volatile Thread thread;
		//該節點喚醒後依據該節點的狀態判斷是否依據條件喚醒下一個節點
    Node nextWaiter;
		//檢查當前節點是否爲共享節點
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
		//查找前置節點是否存在
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}
狀態 判斷結果 說明
waitStatus=0 代表初始化狀態 該節點尚未被初始化完成
waitStatus>0 取消狀態 說明該線程中斷或者等待超時,需要移除該線程
waitStatus<0 有效狀態 該線程處於可以被喚醒的狀態
nextWaiter狀態標誌 說明
SHARED(共享模式) 直接喚醒下一個節點
EXCLUSIVE(獨佔模式) 等待當前線程執行完成後再喚醒
其他非空值 依據條件決定怎麼喚醒下一個線程。類似semaphore中控制幾個線程通過

​ 首先確定自己是否爲頭部節點,如果是頭部節點則直接獲取資源開始執行,如果不是則自旋前置節點直到前置節點執行完成狀態修改爲CANCELLED,然後斷開前置節點的鏈接,獲取資源開始執行。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章