Java深海拾遺系列(10)--- Java併發之AQS源碼分析

AQS 全稱是 AbstractQueuedSynchronizer,顧名思義,是一個用來構建鎖和同步器的框架,它底層用了 CAS 技術來保證操作的原子性,同時利用 FIFO 隊列實現線程間的鎖競爭,將基礎的同步相關抽象細節放在 AQS,這也是 ReentrantLock、CountDownLatch 等同步工具實現同步的底層實現機制。它能夠成爲實現大部分同步需求的基礎,也是 J.U.C 併發包同步的核心基礎組件。

AQS 結構剖析

AQS 就是建立在 CAS 的基礎之上,增加了大量的實現細節,例如獲取同步狀態、FIFO 同步隊列,獨佔式鎖和共享式鎖的獲取和釋放等等,這些都是 AQS 類對於同步操作抽離出來的一些通用方法,這麼做也是爲了對實現的一個同步類屏蔽了大量的細節,大大降低了實現同步工具的工作量,這也是爲什麼 AQS 是其它許多同步類的基類的原因。

現在我們來直接定位到類 java.util.concurrent.locks.AbstractQueuedSynchronizer,下面是 AQS 類的幾個重要字段與方法列出來:


public abstract class AbstractQueuedSynchronizer
  extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private transient volatile Node head;

    private transient volatile Node tail;

    private volatile int state;

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    // ...

 }

1.head 字段爲等待隊列的頭節點,表示當前正在執行的節點;2.tail 字段爲等待隊列的尾節點;3.state 字段爲同步狀態,其中 state > 0 爲有鎖狀態,每次加鎖就在原有 state 基礎上加 1,即代表當前持有鎖的線程加了 state 次鎖,反之解鎖時每次減一,當 statte = 0 爲無鎖狀態;4.通過 compareAndSetState 方法操作 CAS 更改 state 狀態,保證 state 的原子性。

有沒有發現,這幾個字段都用 volatile 關鍵字進行修飾,以確保多線程間保證字段的可見性。

AQS 提供了兩種鎖,分別是獨佔鎖和共享鎖,獨佔鎖指的是操作被認作一種獨佔操作,比如 ReentrantLock,它實現了獨佔鎖的方法,而共享鎖則指的是一個非獨佔操作,比如一些同步工具 CountDownLatch 和 Semaphore 等同步工具,下面是 AQS 對這兩種鎖提供的抽象方法。

獨佔鎖:

// 獲取鎖方法protected boolean tryAcquire(int arg) {  throw new UnsupportedOperationException();}// 釋放鎖方法protected boolean tryRelease(int arg) {  throw new UnsupportedOperationException();}

共享鎖:

// 獲取鎖方法protected int tryAcquireShared(int arg) {  throw new UnsupportedOperationException();}// 釋放鎖方法protected boolean tryReleaseShared(int arg) {  throw new UnsupportedOperationException();}

在我們平時開發中,基本不用直接使用 AQS,我們平時都是直接使用 JDK 自帶的同步類工具,如 ReentrantLock、CountDownLatch 和 Semaphore 等,它們已經可以滿足絕大部分的需求了,後面會抽幾篇文章單獨講一下這些同步類工具是如何使用 AQS 的,這對於我們如何構建自定義的同步工具,有很大的幫助。

下面是同步隊列節點的結構:

 

用大神的註釋來形象地描述一下隊列的模型:

/**  * <pre>  *      +------+  prev +-----+       +-----+  * head |      | <---- |     | <---- |     |  tail  *      +------+       +-----+       +-----+  * </pre>  */

這是一個普通雙向鏈表的節點結構,多了 thread 字段用於存儲當前線程對象,同時每個節點都有一個 waitStatus 等待狀態,一共有四種狀態:

1.CANCELLED(1):取消狀態,如果當前線程的前置節點狀態爲 CANCELLED,則表明前置節點已經等待超時或者已經被中斷了,這時需要將其從等待隊列中刪除。2.SIGNAL(-1):等待觸發狀態,如果當前線程的前置節點狀態爲 SIGNAL,則表明當前線程需要阻塞。3.CONDITION(-2):等待條件狀態,表示當前節點在等待 condition,即在 condition 隊列中。4.PROPAGATE(-3):狀態需要向後傳播,表示 releaseShared 需要被傳播給後續節點,僅在共享鎖模式下使用。

可以這麼理解:head 節點可以表示成當前持有鎖的線程的節點,其餘線程競爭鎖失敗後,會加入到隊尾,tail 始終指向隊列的最後一個節點。

AQS 的結構大概可總結爲以下 3 部分:

1.用 volatile 修飾的整數類型的 state 狀態,用於表示同步狀態,提供 getState 和 setState 來操作同步狀態;2.提供了一個 FIFO 等待隊列,實現線程間的競爭和等待,這是 AQS 的核心;3.AQS 內部提供了各種基於 CAS 原子操作方法,如 compareAndSetState 方法,並且提供了鎖操作的acquire和release方法。

獨佔鎖

獨佔鎖的原理是如果有線程獲取到鎖,那麼其它線程只能是獲取鎖失敗,然後進入等待隊列中等待被喚醒。

獲取鎖

獲取獨佔鎖方法:

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

源碼解讀:

1.通過 tryAcquire(arg) 方法嘗試獲取鎖,這個方法需要實現類自己實現獲取鎖的邏輯,獲取鎖成功後則不執行後面加入等待隊列的邏輯了;2.如果嘗試獲取鎖失敗後,則執行 addWaiter(Node.EXCLUSIVE) 方法將當前線程封裝成一個 Node 節點對象,並加入隊列尾部;3.把當前線程執行封裝成 Node 節點後,繼續執行 acquireQueued 的邏輯,該邏輯主要是判斷當前節點的前置節點是否是頭節點,來嘗試獲取鎖,如果獲取鎖成功,則當前節點就會成爲新的頭節點,這也是獲取鎖的核心邏輯。

基於上面源碼的步驟分析後,我們一步步往下看源碼具體實現:

private Node addWaiter(Node mode) {  // 創建一個基於當前線程的節點,該節點是 Node.EXCLUSIVE 獨佔式類型  Node node = new Node(Thread.currentThread(), mode);  // Try the fast path of enq; backup to full enq on failure  Node pred = tail;  // 這裏先判斷隊尾是否爲空,如果不爲空則直接將節點加入隊尾  if (pred != null) {    node.prev = pred;    // 採取 CAS 操作,將當前節點設置爲隊尾節點,由於採用了 CAS 原子操作,無論併發怎麼修改,都有且只有一條線程可以修改成功,其餘都將執行後面的enq方法    if (compareAndSetTail(pred, node)) {      pred.next = node;      return node;    }  }  enq(node);  return node;}

簡單來說 addWaiter(Node mode) 方法做了以下事情:

1.創建基於當前線程的獨佔式類型的節點;2.利用 CAS 原子操作,將節點加入隊尾。

我們繼續看 enq(Node node) 方法:

private Node enq(final Node node) {  // 自旋操作  for (;;) {    Node t = tail;    // 如果隊尾節點爲空,那麼進行CAS操作初始化隊列    if (t == null) {      // 這裏很關鍵,即如果隊列爲空,那麼此時必須初始化隊列,初始化一個空的節點表示隊列頭,用於表示當前正在執行的節點,頭節點即表示當前正在運行的節點      if (compareAndSetHead(new Node()))        tail = head;    } else {      node.prev = t;      // 這一步也是採取CAS操作,將當前節點加入隊尾,如果失敗的話,自旋繼續修改直到成功爲止      if (compareAndSetTail(t, node)) {        t.next = node;        return t;      }    }  }}

enq(final Node node) 方法主要做了以下事情:

1.採用自旋機制,這是 aqs 裏面很重要的一個機制;2.如果隊尾節點爲空,則初始化隊列,將頭節點設置爲空節點,頭節點即表示當前正在運行的節點;3.如果隊尾節點不爲空,則繼續採取 CAS 操作,將當前節點加入隊尾,不成功則繼續自旋,直到成功爲止;

對比了上面兩段代碼,不難看出,首先是判斷隊尾是否爲空,先進行一次 CAS 入隊操作,如果失敗則進入 enq(final Node node) 方法執行完整的入隊操作。

完整的入隊操作簡單來說就是:如果隊列爲空,初始化隊列,並將頭節點設爲空節點,表示當前正在運行的節點,然後再將當前線程的節點加入到隊列尾部。

關於隊列的初始化與入隊,務必理解透徹。

經過上面 CAS 不斷嘗試,這時當前節點已經成功加入到隊尾了,接下來就到了acquireQueued 的邏輯,我們繼續往下看源碼:

final boolean acquireQueued(final Node node, int arg) {  boolean failed = true;  try {    // 線程中斷標記字段    boolean interrupted = false;    for (;;) {      // 獲取當前節點的 pred 節點      final Node p = node.predecessor();      // 如果 pred 節點爲 head 節點,那麼再次嘗試獲取鎖      if (p == head && tryAcquire(arg)) {        // 獲取鎖之後,那麼當前節點也就成爲了 head 節點        setHead(node);        p.next = null; // help GC        failed = false;        // 不需要掛起,返回 false        return interrupted;      }      // 獲取鎖失敗,則進入掛起邏輯      if (shouldParkAfterFailedAcquire(p, node) &&          parkAndCheckInterrupt())        interrupted = true;    }  } finally {    if (failed)      cancelAcquire(node);  }}

這一步 acquireQueued(final Node node, int arg) 方法主要做了以下事情:

1.判斷當前節點的 pred 節點是否爲 head 節點,如果是,則嘗試獲取鎖;2.獲取鎖失敗後,進入掛起邏輯。

提醒一點:我們上面也說過,head 節點代表當前持有鎖的線程,那麼如果當前節點的 pred 節點是 head 節點,很可能此時 head 節點已經釋放鎖了,所以此時需要再次嘗試獲取鎖。

接下來繼續看掛起邏輯源碼:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {  int ws = pred.waitStatus;  if (ws == Node.SIGNAL)    // 如果 pred 節點爲 SIGNAL 狀態,返回true,說明當前節點需要掛起    return true;  // 如果ws > 0,說明節點狀態爲CANCELLED,需要從隊列中刪除  if (ws > 0) {    do {      node.prev = pred = pred.prev;    } while (pred.waitStatus > 0);    pred.next = node;  } else {    // 如果是其它狀態,則操作CAS統一改成SIGNAL狀態    // 由於這裏waitStatus的值只能是0或者PROPAGATE,所以我們將節點設置爲SIGNAL,從新循環一次判斷    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  }  return false;}

這一步 shouldParkAfterFailedAcquire(Node pred, Node node) 方法主要做了以下事情:

1.判斷 pred 節點狀態,如果爲 SIGNAL 狀態,則直接返回 true 執行掛起;2.刪除狀態爲 CANCELLED 的節點;3.若 pred 節點狀態爲 0 或者 PROPAGATE,則將其設置爲爲 SIGNAL,再從 acquireQueued 方法自旋操作從新循環一次判斷。

通俗來說就是:根據 pred 節點狀態來判斷當前節點是否可以掛起,如果該方法返回 false,那麼掛起條件還沒準備好,就會重新進入 acquireQueued(final Node node, int arg) 的自旋體,重新進行判斷。如果返回 true,那就說明當前線程可以進行掛起操作了,那麼就會繼續執行掛起。

這裏需要注意的時候,節點的初始值爲 0,因此如果獲取鎖失敗,會嘗試將節點設置爲 SIGNAL。

繼續看掛起邏輯:

private final boolean parkAndCheckInterrupt() {  LockSupport.park(this);  return Thread.interrupted();}

LockSupport 是用來創建鎖和其他同步類的基本線程阻塞原語。LockSupport 提供 park() 和 unpark() 方法實現阻塞線程和解除線程阻塞。release 釋放鎖方法邏輯會調用 LockSupport.unPark 方法來喚醒後繼節點。

獲取獨佔鎖流程圖:

 

釋放鎖

釋放鎖方法:

public final boolean release(int arg) {  if (tryRelease(arg)) {    Node h = head;    if (h != null && h.waitStatus != 0)      unparkSuccessor(h);    return true;  }  return false;}

釋放鎖的方法源碼就很好理解,通過 tryRelease(arg) 方法嘗試釋放鎖,這個方法需要實現類自己實現釋放鎖的邏輯,釋放鎖成功後則執行後面的喚醒後續節點的邏輯了,然後判斷 head 節點不爲空並且 head 節點狀態不爲 0,因爲 addWaiter 方法默認的節點狀態爲 0,此時節點還沒有進入就緒狀態。

繼續往下看源碼:

private void unparkSuccessor(Node node) {  int ws = node.waitStatus;  if (ws < 0)    // 將頭節點的狀態設置爲0    // 這裏會嘗試清除頭節點的狀態,改爲初始狀態    compareAndSetWaitStatus(node, ws, 0);
  // 後繼節點  Node s = node.next;  // 如果後繼節點爲null,或者已經被取消了  if (s == null || s.waitStatus > 0) {    s = null;    // for循環從隊列尾部一直往前找可以喚醒的節點    for (Node t = tail; t != null && t != node; t = t.prev)      if (t.waitStatus <= 0)        s = t;  }  if (s != null)    // 喚醒後繼節點    LockSupport.unpark(s.thread);}

從源碼可看出:釋放鎖主要是將頭節點的後繼節點喚醒,如果後繼節點不符合喚醒條件,則從隊尾一直往前找,直到找到符合條件的節點爲止

總結

這篇文章主要講述了 AQS 的內部結構和它的同步實現原理,並從源碼的角度深度剖析了AQS 獨佔鎖模式下的獲取鎖與釋放鎖的邏輯,並且從源碼中我們得出:在獨佔鎖模式下,用 state 值表示鎖並且 0 表示無鎖狀態,0 -> 1 表示從無鎖到有鎖,僅允許一條線程持有鎖,其餘的線程會被包裝成一個 Node 節點放到隊列中進行掛起,隊列中的頭節點表示當前正在執行的線程,當頭節點釋放後會喚醒後繼節點,從而印證了 AQS 的隊列是一個 FIFO 同步隊列。

 

推薦閱讀:

從源碼的角度解析線程池運行原理

關於線程池你不得不知道的一些設置

你都理解創建線程池的參數嗎?

 

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