《Java後端知識體系》系列之AQS詳解

概覽:

  • 在併發編程中不得不提到的就是AQS(AbstractQueueSynchronizer)抽象同步隊列,它是實現同步器的基礎組件,併發包中鎖的底層實現就是使用AQS來實現的。AQS的結構圖如下:

在這裏插入圖片描述

  • 從該圖中可以看到AQS是一個FIFO(先進先出)雙向隊列,其內部節點通過head和tail記錄隊首和隊尾元素,隊列元素的類型爲Node,其中Node中的thread變量用來存放AQS隊列中的線程;Node節點內部的SHARED用來標記該線程是獲取共享資源時被阻塞掛起後放入AQS隊列的,EXCLUSIVE用來標記線程是獲取獨佔資源時被掛起後放入AQS隊列的;waitStatus是記錄當前線程的等待狀態,可以爲CANCELLED(線程被取消)、SIGNAL(線程需要被喚醒)、CONDITION(線程在條件隊列中等待)、PROPAGATE(釋放共享資源時需要通知其它節點);prev記錄當前節點的前驅節點,next記錄當前節點的後繼節點。
  • 在AQS中維護了一個單一的狀態信息state,可以通過getState、setState、CompareAndSetState函數來修改其值。
    • 對於ReentrantLock的實現來說,state可以用來表示當前線程獲取鎖的可重入次數
    • 對於ReentrantReadWriteLock來說,state高16爲表示狀態,也就是獲取該讀鎖的次數,低16位表示獲取到寫鎖的的線程的可重入次數;
    • 對於Semaphore來說,state用來表示當前可用信號的個數,
    • 對於CountDownlatch來說,state用來表示計數器當前的值。
  • AQS有個內部類ConditionObject,用來結合鎖實現線程同步。ConditionObject可以直接訪問AQS對象內部的變量,比如state狀態值和AQS隊列,ConditionObject是條件變量,每個條件變量對應一個條件隊列(單向鏈表隊列),其用來存放調用條件變量的await方法後被阻塞的線程。
  • 對於AQS來說,線程同步的關鍵是對狀態值state進行操作,根據state是否屬於一個線程,操作state的方式分爲

在獨佔方式下獲取和釋放資源的方法爲:

void acquire( int arg) 
void acquirelnterruptibly(int arg)
boolean release( int arg)
  • 使用獨佔鎖方式獲取資源是跟線程綁定的,就是說如果一個線程獲取到了資源,就會標記是這個線程獲取了,其它線程再嘗試操作state獲取資源時發現當前資源不是自己持有的,就會獲取失敗後被阻塞。比如獨佔鎖ReentrantLock的實現,當一個線程獲取了ReentrantLock的鎖後,在AQS內部會首先使用CAS操作把state狀態值從0變爲1,然後設置當前鎖的持有者爲當前線程,當該線程再次獲取鎖時發現鎖的持有者是自己,那麼就會把state加1,也就是可以設置重入次數,而當另一個線程獲取鎖時發現自己並不是該鎖的持有者就會被放入AQS阻塞隊列後掛起。

在共享方式下獲取和釋放資源的方法爲:

void acquireShared(int arg) 
void acquireSharedinterruptibly(int arg)
boolean reeaseShared(int arg)
  • 對於共享式來說,當多個線程去請求共享資源時通過CAS方式競爭獲取資源,當一個線程獲取到資源之後,另一個線程再去獲取時如果當前資源還能滿足它的需要,則當前線程只需要通過CAS方法進行獲取即可。比如Semaphore信號量,當一個線程通過acquire()方式獲取信號量時,會首先查看當前信號量個數是否滿足需要,不滿足則把當前線程放入阻塞隊列,如果滿足則通過自旋CAS獲取信號量。

在獨佔方式下,獲取與釋放資源的流程如下:

  1. 當一個線程調用acquire(int arg)方法獲取獨佔資源時,會首先使用tryAcquire方法嘗試獲取資源,具體時設置狀態變量state的值,成功則直接返回,失敗則將當前線程封裝爲Node.EXCLUSIVE的Node節點後插入AQS阻塞隊列的尾部,並調用LockSupport.park(this)方法掛起自己。

    public final void acquire(int arg){
    	if(!tryAcquire(arg)&&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
    	selfInterrupt();
    }
    
  2. 當一個線程調用release(int arg)方法時會嘗試使用tryRelease操作釋放資源,這裏是設置狀態變量state的值,然後調用LockSupport.unpark(thread)方法激活AQS隊列裏面被阻塞的一個線程(thread)。被激活的線程則使用tryAcquire嘗試,看當前狀態變量state的值是否滿足自己的需要,滿足則激活該線程,然後繼續向下運行,否則還會被放入AQS隊列並掛起。

    public final boolean release (int arg) {
    	if (tryRelease(arg )) {
    		Node h = head;
    		if (h 1= null && h .waitStatus != 0)
    			unparkSuccessor(h);
    			return true ;
    		}
    	return false; 
    }
    
  3. 需要注意的是,AQS類並沒有提供可用的tryAcquire和tryRelease方法,正如AQS的鎖阻塞和同步器的基礎框架一樣,tryAcquire和tryRelease需要具體的子類來實現。子類在實現tryAcquire和tryRelease時要根據具體場景使用CAS算法嘗試修改state的值,成功則返回true,否則返回false。子類還需要定義在調用acquire和release方法時state狀態值的增減代表什麼含義。

  4. 比如繼承自AQS實現的獨佔鎖ReentrantLock,定義當state爲0時表示鎖空閒,不爲0時表示鎖已經被佔用,在重寫tryAcquire時,在內部需要使用CAS算法查看當前state是否爲0,如果爲0則使用CAS設置爲1,並設置鎖的持有者爲當前線程,而後返回true,如果CAS失敗則返回false。

在共享方式下獲取與釋放鎖的流程如下:

  1. 當線程調用acquireShared(int arg)獲取共享資源時,會首先使用trγAcquireShared嘗試獲取資源,具體是設置狀態變量state的值,成功則直接返回,失敗則將當前線程封裝爲Node.SHARED的Node節點後插入到AQS阻塞隊列的尾部,並使用LockSupport.park(this)方法掛起自己。

    public final void acquireShared(int arg){
    	if(tryAcquireShared(arg)<0)
    		doAcquireShared(arg)	
    }
    
  2. 當一個線程調用releaseShared(int arg)時會嘗試使用tryReleaseShared操作釋放資源,這裏是設置狀態變量state的值,然後使用LockSupport.unpark(thread)激活AQS隊列裏面被阻塞的一個線程(thread)。被激活的線程則使用tryReleaseShared查看當前狀態變量state的值是否滿足自己的需要,滿足則激活該線程,然後繼續向下運行,否則還是放入AQS的阻塞隊列並掛起。

    public final boolean releaseShared(int arg){
    	if(tryReleaseShared(arg)){
    		doReleaseShared();
    		return true;
    	}
    	return false;
    }
    
  3. 同樣需要注意的是,AQS類並沒有提供可用tryAcquireSharedtryReleaseShared方法,正如AQS是鎖阻塞和同步器的基礎框架一樣,tryAcquireShared和tryReleaseShared需要由具體的子類來實現。子類在實現tryAcquireShared和tryReleaseShared時要根據具體場景使用CAS算法嘗試修改state的值,成功則返回true,否則返回false。

  4. 比如繼承自AQS實現的讀寫鎖ReentrantReadWriteLock裏面的讀鎖在重寫tryAcquireShared時,首先查看寫鎖是否被其它線程持有,如果是則直接返回false,否則使用CAS遞增state的高16位(在ReentrantReadWriteLock中,state的高16位爲讀鎖的次數。)在重寫tryReleaseShared時,其內部使用CAS算法把當前state的值高16位減1,然後返回true,否則返回false。

以上就是自己整理的AQS的一部分知識,AQS的知識點很多慢慢整理。我是會敲代碼的湯姆貓!

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