隊列同步器(AQS)的設計原理

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,便可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程文章。編程

微信公衆號

1. 前言

  • 在Java中鎖所能夠分爲兩大類,一類是經過synchrinized關鍵字實現的隱式鎖,一類是JUC包的鎖。前者是經過JVM實現的,後者是根據隊列同步器(AQS)實現的,也就是今天的主角。
  • 在JUC包下實現了不少鎖以及工具類,例如ReentrantLock、ReadWriteLock、CountDownLatch、CyclicBarrier等,均是經過隊列同步器實現的,因此理解了隊列同步器的實現原理,對使用這些鎖及工具類或者閱讀這些類的源碼會有很大幫助。

2. 什麼是AQS

  • 隊列同步器的全稱是AbstractQueuedSynchronizer,簡稱AQS,翻譯過來就是抽象的隊列同步器。從命名就能猜出,這個類是一個抽象類,且是基於隊列來實現的一個同步器。JUC包下全部的鎖都是基於它來實現的。在AQS中定義了一個int類型的變量:state,用它來表示同步狀態,哪一個線程成功對state變量進行了加1操做,那麼這個線程就持有了鎖;AQS中還定義了一個FIFO(先進先出)的隊列,用來表示等待獲取鎖的線程。
  • 在計算機領域,鎖的實現均可以用管程模型來實現。管程模型的示意圖以下,在管程模型中存在兩個概念:入口等待隊列和條件等待隊列。既然鎖均可以來管程來實現,那麼JUC包下實現的鎖中是否是也存在這入口等待隊列條件等待隊列呢?答案是確定的。AQS中也存着兩個隊列:同步隊列條件等待隊列,它們分別對應管程中的入口等待隊列條件等待隊列。今天先分析AQS中的同步隊列的數據結構和實現原理,關於AQS中條件等待隊列會在Condition類的源碼分析中講解。
  • 關於管程的介紹能夠參考這篇文章:管程:併發編程的基石。也能夠閱讀極客時間上《Java併發編程實戰》一課中的第一部分第8講:管程:併發編程的萬能鑰匙 【圖】

3. AQS中的方法

  • AQS類提供了不少方法,既然是一個抽象類,就會有方法須要子類去重寫。AQS中有以下方法須要子類重寫。
方法 做用
protected boolean tryAcquire(int arg) 獨佔式嘗試獲取鎖
protected boolean tryRelease(int arg) 獨佔式嘗試釋放鎖
protected int tryAcquireShared(int arg) 共享式嘗試獲取鎖
protected boolean tryReleaseShared(int arg) 共享式嘗試釋放鎖
protected boolean isHeldExclusively() 當前線程是否獨佔式的佔用鎖
  • 上面子類重寫的方法中,獲取或者釋放鎖時都會嘗試去修改同步狀態state的值,在AQS中提供了三個和同步狀態相關的方法。(上面的方法說明中都是用嘗試二字,這是由於調用這些方法不必定能獲取鎖成功或者釋放鎖成功)
方法 做用
int getState() 獲取同步狀態state的值
void setState(int newState) 修改同步狀態。一般是隻有已經獲取到鎖的線程才調用這個方法去修改同步狀態,這個時候由於只有一個線程能取到鎖,因此不用擔憂併發問題
boolean compareAndSetState(int expect, int update) 經過CAS的方式去修改同步狀態,當多個線程同時嘗試修改state時使用,它能保證只有一個線程能修改爲功
  • AQS的設計採用了模板設計模式,它定義了不少模板方法,在模板方法中會調用由子類重寫的方法。這樣就抽象出了鎖實現的通用邏輯,而針對不一樣類型的鎖的實現,只須要有給不一樣類型鎖的同步組件在重寫的方法中實現本身特有的邏輯便可。下面列出部分模板方法及其做用。
方法 做用
void acquire(int arg) 獨佔式獲取同步狀態,若是線程成功獲取了同步狀態,則方法會返回,若是沒有獲取到同步狀態,那麼當前線程就會進入到同步隊列中,並阻塞。該方法對中斷沒法響應
void acquireInterruptibly(int arg) throws InterruptedException acquire()方法同樣,不過該方法能響應中斷
boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException acquireInterruptibly()方法的基礎上增長了超時限制,當指定時間內若是沒有獲取到同步鎖,就會返回false。
void acquireShared(int arg) 共享式獲取同步狀態,若是線程成功獲取到了同步狀態,那麼方法就會返回。不然進入到同步隊列中進行等待,並阻塞。它與acquire()的區別是,該方法能容許多個線程同時獲取到鎖
void acquireSharedInterruptibly(int arg) throws InterruptedException acquireShared()方法的基礎上增長了響應中斷的功能
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException acquireSharedInterruptibly()基礎上增長了超時功能,在指定時間內若是沒有獲取到鎖,就會返回false
boolean release(int arg) 獨佔式釋放鎖
boolean releaseShared(int arg) 共享式釋放鎖
  • 從上面的方法中能夠發現,這幾個模板都是成對出現的,獨佔式和共享式獲取鎖,能響應中斷的獨佔式和共享式獲取鎖,能超時的獨佔式和共享式獲取鎖,釋放獨佔式和共享式鎖。因此實際上咱們只須要弄明白acquire()方法和release()方法便可,其餘的方法與這兩個方法的實現幾乎同樣,只是改變了部分邏輯。
  • 獨佔式和共享式的區別:獨佔式表示的是隻能有一個線程獲取到鎖,而共享式表示的是同一時刻容許有多個線程獲取到鎖。前者的實際應用有ReentrantLock,後者的實際應用有ReentrantReadWriteLock、CountDownLatch、CyclicBarrier等。這些類的源碼以及實現原理後面會有文章專門分析。

4. 同步隊列的設計原理

要想讀懂AQS的源代碼,首先須要明白它的設計原理,不然很難看明白其中的邏輯。畢竟代碼只是具體實現的工具,編程語言能夠多變,但設計原理是不變的。設計模式

4.1 數據結構

  • AQS中兩大核心:同步狀態同步隊列。同步狀態由state這個int類型的全局變量實現,哪一個線程成功修改了state的值,就表示這個線程獲取到了鎖或者釋放了鎖。同步隊列是一個遵循先進先出(FIFO)的隊列,它是一個由Node節點組成的雙向鏈表。每個線程在獲取同步狀態時,若是獲取同步狀態失敗,就會將當前線程封裝成一個Node,而後將其加入到同步隊列中。Node是AQS裏面的一個靜態內部類,Node這個數據結構中,包含了5個屬性,每一個屬性的功能以下列表。Node就是經過這5個屬性來實現同步隊列等待隊列的,關於等待隊列今天先暫時不分析,後面在分析Condition源碼時會詳細分析。
屬性名 做用
Node prev 同步隊列中,當前節點的前一個節點,若是當前節點是同步隊列的頭結點,那麼prev屬性爲null
Node next 同步隊列中,當前節點的後一個節點,若是當前節點是同步隊列的尾結點,那麼next屬性爲null
Node thread 當前節點表明的線程,若是當前線程獲取到了鎖,那麼當前線程所表明的節點必定處於同步隊列的隊首,且thread屬性爲null,至於爲何要將其設置爲null,這是AQS特地設計的。
int waitStatus 當前線程的等待狀態,有5種取值。0表示初始值,1表示線程被取消,-1表示當前線程處於等待狀態,-2表示節點處於等待隊列中,-3表示下一次共享式同步狀態獲取將會無條件地被傳播下去
Node nextWaiter 等待隊列中,該節點的下一個節點
  • 經過Node的prev屬性和next屬性就構成了一個雙向鏈表,也就是AQS中的同步隊列,可是想要經過這個隊列找到隊列中的每個元素,咱們就須要知道這個隊列的頭結點是誰,尾結點是誰。所以AQS中又提供了兩個屬性:headtail,這兩個屬性的類型均是Node類型,它們分別指向同步隊列中的頭結點和尾結點。這樣AQS就能經過head和tail,找到隊列中的每個元素。同步隊列的結構示意圖以下。

同步隊列示意圖

4.2 實現原理

  • 當一個線程調用acquire()方法獲取同步狀態的時候,若是此時能成功獲取到同步狀態,那麼就直接返回;若是不能獲取到同步狀態,此時就表示同步狀態已經被其餘線程獲取到了,那麼這個時候,當前線程就須要開始等待,那麼如何實現等待呢?此時當前線程先現將本身封裝成一個Node,而後這個Node加入到同步隊列中。在加入到同步隊列以前,須要判斷隊列有沒有被初始化,即隊列中有沒有節點存在。若是head=null則表示當前同步隊列尚未初始化,因此這個時候當前線程作的第一件事,就是初始化隊列。如何初始化呢?當前線程須要先初始化head節點,所以它會new一個Node,而後將這個Node賦值給head,注意head節點表示的是獲取到同步狀態的線程。接着當前線程再將本身封裝成一個Node,而後將head的next屬性指向這個Node,這樣就將本身加入到了隊列中。注意head節點的thread屬性始終都是null,由於head節點是當前線程建立的,而當前線程只知道有線程獲取到了同步狀態,可是殊不知道是誰獲取到了,因此此時當前線程在初始化head節點的時候,只能讓head節點的thread屬性爲null。當前線程再將本身加入到隊列以後,還須要將tail指向本身。在設置head屬性和tail屬性時,因爲存在多個線程併發的可能,因此須要使用AQS提供的compareAndSetHead()、compareAndSetTail()方法,這兩個方法會調用Unsafe類的CAS方法,能保證原子性。節點加入到同步隊列的示意圖以下。

節點加入到隊列示意圖

  • 同步隊列中首節點表示的是獲取到同步狀態的線程,當首節點表明的線程釋放了同步狀態,因爲AQS遵循FIFO,因此此時線程在釋放同步狀態後還須要喚醒後面節點的線程去獲取同步狀態。當有線程獲取到同步狀態後,須要將本身表明的節點設置爲同步隊列的首節點。因爲此時確定只有一個線程獲取到同步狀態,所以此時在更新head屬性時,不須要經過CAS方法來保證原子性,只須要使用setHead()方法便可。首節點的設置的示意圖以下。

首節點設置示意圖

5. 總結

  • 本文主要介紹了AQS中各類API的做用,以及分類。最後經過對Node的數據結構分析了AQS的設計原理。
  • 下一篇文章將結合具體的源碼來分析AQS是如何實現鎖。

微信公衆號
相關文章
相關標籤/搜索