天天看點

多線程(十五)AQS介紹

前言

  1995年sun公司釋出了第一個Java語言版本,可以說從jdk1.1到jdk1.4期間java的使用主要是在移動應用和中小型企業應用中,在此類領域中基本不用設計大型并發場景,當然也沒有大型網際網路公司使用java,因為擔心它本身的性能。在網際網路及伺服器硬體迅猛的發展下,sun公司更加注重企業級應用方面,毫無疑問高并發是一個重要的主題,于是在J2SE5.0(jdk1.5)代号為老虎的版本中增加了更加強大的并發相關的操作包——java.util.concurrent。此後java在高并發中表現優異,很多大型網際網路公司都使用java作為主要開發語言,例如阿裡巴巴、ebay等,這些公司系統的通路絕對是屬于世界級的大型并發場景,反映了java在大型并發場景是可行的。Jdk的并發包提供了各種鎖及同步機制,其實作的核心類是AbstractQueuedSynchronizer,我們簡稱為AQS架構,它為不同場景提供了實作鎖及同步機制的基本架構,為同步狀态的原子性管理、線程的阻塞、線程的解除阻塞及排隊管理提供了一種通用的機制。

  Jdk的并發包(juc)的作者是Doug Lea,但其中思想卻是結合了多位大師的智慧,如果你想深入了解juc的相關理論可以參考Doug Lea寫的《The_java.util.concurrent_Synchronizer_Framework》論文。從這裡可以找到AQS的理論基礎,包括架構的基本原理、需求、設計、實作思路、用法及性能,由于這些方面篇幅較大,本文不打算涉及所有方面,主要将針對AQS類的結構及相關操作進行分析。

  ASQ将線程封裝到一個Node裡面,并維護一個CHL Node FIFO隊列,它是一個非阻塞的FIFO隊列,也就是說在并發條件下往此隊列做插入或移除操作不會阻塞,是通過自旋鎖和CAS保證節點插入和移除的原子性,實作無鎖快速插入。

  其實AbstractQueuedSynchronizer主要就是維護了一個state屬性、一個FIFO隊列和線程的阻塞與解除阻塞操作。state表示同步狀态,它的類型為32位整型,對state的更新必須要保證原子性。這裡的隊列是一個雙向連結清單,每個節點裡面都有一個prev和next,它們分别是前一個節點和後一個節點的引用。需要注意的是此雙向連結清單除了鍊頭其他每個節點内部都包含一個線程,而鍊頭可以了解為一個空節點。

多線程(十五)AQS介紹
對于隊列的結構我們需要深入了解下,下圖展示的是組成雙向連結清單其中一個節點的結構,該節點包含五個主要元素,表示的意思如下表:
           

            

多線程(十五)AQS介紹
屬性 含義
Node prev 前驅節點,指向前一個節點
Node next 後續節點,指向後一個節點
Node nextWaiter 用于存儲condition隊列的後續節點
Thread thread 入隊列時的目前線程
int waitStatus

有五種狀态:

① SIGNAL,值為-1,表示目前節點的後續節點中的線程通過park被阻塞了,目前節點在釋放或取消時要通過unpark解除它的阻塞。

② CANCELLED,值為1,表示目前節點的線程因為逾時或中斷被取消了。

③ CONDITION,值為-2,表示目前節點在condition隊列中。

④ PROPAGATE,值為-3,共享模式的頭結點可能處于此狀态,表示無條件往下傳播,引入此狀态是為了優化鎖競争,使隊列中線程有序地一個一個喚醒。

⑤ 0,除了以上四種狀态的第五種狀态,一般是節點初始狀态。

  前驅節點prev的引入主要是為了完成逾時及取消語義,前驅節點取消後隻需向前找到一個未取消的前驅節點即可;後續節點的引入主要是為了優化後續節點的查找,避免每次從尾部向前查找;nextWaiter用于表示condition隊列的後續節點,此時prev和next屬性将不再使用,而且節點狀态處于Node.CONDITION; waitStatus表示的是後續節點狀态,這是因為AQS中使用CLH隊列實作線程的結構管理,而CLH結構正是用前一節點某一屬性表示目前節點的狀态,這樣更容易實作取消和逾時功能。

分析

  在前面部落格中,LZ講到了ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch,他們都有各自擷取鎖的方法,同時相對于Java的内置鎖,他們具有明顯的優勢:花最小的空間開銷建立鎖、最少的時間開銷獲得鎖、使用更加友善靈活。

  參考Java的内置鎖,對于JUC同步器而言,他應該具備兩個最基本的功能:擷取鎖,釋放鎖。其中擷取鎖應該是先判斷目前狀态是否可以擷取,如果不可以擷取則處于阻塞狀态,釋放應該是釋放後修改狀态,讓其他線程能夠得到該鎖(喚醒其他線程),如下:

lock:  
        while(state){                //  
            getLock();            //擷取鎖  
            return currentThread;        //傳回目前線程  
        }  

        release:  
        updateState();                //修改狀态      
        notify other thread;            //喚醒其他線程  
           

  我們知道,在JUC中,各個同步器擷取鎖和釋放鎖的方法都不相同,比如:lock.lock()、Semaphore.acquire()、 CountDownLatch.await()等等,假如他們都各自實作各自的方法,那麼這個JUC架構頂多隻能算一個中下等的架構設計了。這是AQS騰空出世,AQS作為一個核心的處理架構,他提供了大量的同步操作,同時使用者還可以在此類的基礎上進行自定義,實作自己的同步器。其主要架構如下:

多線程(十五)AQS介紹

  從上圖可以看出AQS是JUC同步器的基石。下面就AQS涉及的技術原理簡單闡述下,以後會對其做詳細的說明。

1、狀态位state

AQS用的是一個32位的整型來表示同步狀态的,它是用volatile修飾的:

private volatile int state;  
           

在互斥鎖中它表示着線程是否已經擷取了鎖,0未擷取,1已經擷取了,大于1表示重入數。同時AQS提供了getState()、setState()、compareAndSetState()方法來擷取和修改該值:

protected final int getState() {  
            return state;  
        }  

        protected final void setState(int newState) {  
            state = newState;  
        }  

        protected final boolean compareAndSetState(int expect, int update) {  
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);  
        }  
           

這些方法需要java.util.concurrent.atomic包的支援,采用CAS操作,保證其原則性和可見性。

2、CLH同步隊列

  在前面就提到過AQS内部維護着一個FIFO的CLH隊列,是以AQS并不支援基于優先級的同步政策。至于為何要選擇CLH隊列,主要在于CLH鎖相對于MSC鎖,他更加容易處理cancel和timeout,同時他具備進出隊列快、無所、暢通無阻、檢查是否有線程在等待也非常容易(head != tail,頭尾指針不同)。當然相對于原始的CLH隊列鎖,ASQ采用的是一種變種的CLH隊列鎖:

1、原始CLH使用的locked自旋,而AQS的CLH則是在每個node裡面使用一個狀态字段來控制阻塞,而不是自旋。

2、為了可以處理timeout和cancel操作,每個node維護一個指向前驅的指針。如果一個node的前驅被cancel,這個node可以前向移動使用前驅的狀态字段。

3、head結點使用的是傀儡結點。

多線程(十五)AQS介紹

3、共享鎖、互斥鎖

  在AQS維護的CLH隊列鎖中,每個節點(Node)代表着一個需要擷取鎖的線程。該Node中有兩個常量SHARE、EXCLUSIVE。其中SHARE代表着共享模式,EXCLUSIVE代表着獨占模式。

static final class Node {  
            /** Marker to indicate a node is waiting in shared mode */  
            static final Node SHARED = new Node();  
            /** Marker to indicate a node is waiting in exclusive mode */  
            static final Node EXCLUSIVE = null;  
            /  

        }  
           

  其中共享模式是允許多個線程可以擷取同一個鎖,而獨占模式則一個鎖隻能被一個線程持有,其他線程必須要等待。

  比如:ReentrantReadWriteLock,我們知道ReentrantReadWriteLock的中也有一個叫Sync的内部類繼承了AQS,而AQS的隊列可以同時存放共享鎖和獨占鎖,對于ReentrantReadWriteLock來說分别代表讀鎖和寫鎖,當隊列中的頭節點為讀鎖時,代表讀操作可以執行,而寫操作不能執行,是以請求寫操作的線程會被挂起,當讀操作依次推出後,寫鎖成為頭節點,請求寫操作的線程被喚醒,可以執行寫操作,而此時的讀請求将被封裝成Node放入AQS的隊列中。如此往複,實作讀寫鎖的讀寫交替進行。

4、阻塞、喚醒

  我們知道在使用Java内置鎖時,可以使用wait、notify方法來阻塞、喚醒線程,但是AQS并沒有采用該模式,而是通過LockSupport.park() 和 LockSupport.unpark() 的本地方法來實作線程的阻塞和喚醒。

參考文章:

深度解析Java8 – AbstractQueuedSynchronizer的實作分析(上);

深度解析Java8 – AbstractQueuedSynchronizer的實作分析(下);

Java并發架構——什麼是AQS架構;

多線程(擴充篇)Unsafe類;

原文位址: Java并發架構——什麼是AQS架構;

【Java并發程式設計實戰】—–“J.U.C”:AQS分析(一)

繼續閱讀