Java併發之AQS同步器學習

KingJack發表於2018-08-22

AQS佇列同步器學習

在學習併發的時候,我們一定會接觸到 JUC 當中的工具,JUC 當中為我們準備了很多在併發中需要用到的東西,但是它們都是基於AQS(AbstractQueuedSynchronizer)佇列同步器來實現的,也就是我們如果能夠去梳理清楚AQS當中的知識點,對我們以後瞭解其他併發功能鍵有很大的幫助。

CLH佇列

佇列同步器(AbstractQueuedSynchronizer),是用來構建鎖或者其他同步元件的基礎框架,它使用了一個int變數來表示同步狀態,通過內建的FIFO佇列來完成資源獲取執行緒的排隊工作,併發包的作者Doug Lea期望她能夠成為實現大部分同步需求的基礎。

而這個內建的佇列就是CLH雙向佇列,當前執行緒如果獲取鎖失敗的時候,會將當前執行緒、狀態等資訊封裝成一個Node節點新增到CLH佇列當中去--也就是一個Node節點其實就是一個執行緒,而當有執行緒釋放時,會喚醒CLH佇列並取其首節點進行再次獲取:

  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;
          
          /** waitStatus value to indicate thread has cancelled */
       //處於取消的等待狀態
       /* 因為超時或中斷就會處於該狀態,並且處於該狀態的節點不會轉變為其他狀態
          處於該狀態的節點不會再次被阻塞*/
          static final int CANCELLED =  1;

          /** waitStatus value to indicate successors thread needs unparking */
         //等待狀態
       /*  表示後繼節點是否需要被喚醒 */
          static final int SIGNAL    = -1;

          /** waitStatus value to indicate thread is waiting on condition */
       /* 該節點處於條件佇列當中,該節點不會用作同步佇列直到設定狀態0用來傳輸時才會移到同步佇列當中,並且加入對同步狀態的獲取 */
          static final int CONDITION = -2;
          /**
           * waitStatus value to indicate the next acquireShared should
           * unconditionally propagate
           */
       /* 表示下一次共享式同步狀態獲取將會無條件地傳播下去 */
          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;
          }
      }
複製程式碼

通過上面對Node節點的原始碼進解說,我想對於之後的內容會有很大的幫助的,因為後面的方法當中會有特別多的狀態判斷。

CLH佇列.jpg

當我們重寫同步器的時候,需要使用同步器的3個方法來訪問和修改同步的狀態。分別是:

  • getState():獲取當前同步狀態

  • setState(int newState):設定當前同步狀態

  • compareAndSetState(int expect, int update):通過CAS來設定當前狀態,該方法可以保證設定狀態操作的原子性

入列

我們在上面既然已經講到了AQS當中維護著的是CLH雙向佇列,並且是FIFO,既然是佇列,那肯定就存在著入列和出列的操作,我們來先從入列看起:

acquire(int arg)方法

該方法是獨佔模式下執行緒獲取同步狀態的入口,如果當前執行緒獲取同步狀態成功,則由該方法返回,如獲取不成功將會進入CLH佇列當中進行等待。

在該方法當中會呼叫重寫的tryAcquire(int arg)方法。

  public final void acquire(int arg) {
      if (!tryAcquire(arg) &&
          acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
          selfInterrupt();
  }
複製程式碼
  • tryAcquire(int arg)

    很多人剛看到這個方法的時候,會不會有種一臉懵逼的感覺,方法體居然只是返回一個異常而已,說好的業務邏輯程式碼呢?

    回到我們一開始說的,AQS實際上只是作為一個同步元件的基礎框架,具體的實現要交由自定義的同步器去自己實現,所以該方法當中只有一句異常。

此方法由使用者自定義的同步器去實現,嘗試獲取獨佔資源,如果成功則返回true,如果失敗則返回false

      protected boolean tryAcquire(int arg) {
          throw new UnsupportedOperationException();
      }
複製程式碼
  • addWaiter(Node mode)

    將當前執行緒新增到CLH佇列的隊尾,並且指定獨佔模式。

    Node有兩種模式,分別是獨佔模式和共享模式,也就是Node.EXCLUSIVENode.SHARED

    private Node addWaiter(Node mode) {
              //將當前執行緒以指定模式來建立Node節點
              Node node = new Node(Thread.currentThread(), mode);
              // Try the fast path of enq; backup to full enq on failure
              Node pred = tail;  //獲取佇列尾部給變數pred
              if (pred != null) {  //若隊尾不為空
                  node.prev = pred;  //將當前節點的前置節點指向原來的tail
                  if (compareAndSetTail(pred, node)) {  //通過CAS將tail設定為Node
                      /*
                      *如果設定成功,表示此操作沒有別的執行緒執行成功
      */ 
                      pred.next = node;  //將原來tail節點的後置節點指向node節點
                      return node;  //返回node節點
                  }
              }
              enq(node);
              return node;
          }
複製程式碼
  • enq(Node )

    該方法是將節點插入到CLH佇列的尾部,並且通過自旋(死迴圈)來保證Node節點的正確新增

      private Node enq(final Node node) {
              for (;;) {  //自旋--死迴圈新增節點
                  Node t = tail;  //獲取原來tial節點至t變數
                  if (t == null) { // Must initialize  佇列為空
                      if (compareAndSetHead(new Node()))  //設定一個空節點作為head節點
                          tail = head;  //head和tail是同一個節點
                  } else {  //佇列不為空的正常情況
                      node.prev = t;  //設定當前節點的前置節點為原tail節點
                      if (compareAndSetTail(t, node)) {  //通過CAS設定當前節點為tail節點
                          t.next = node;  //原tail節點後置節點是當前節點
                          return t;  //返回原tail節點結束迴圈
                      }
                  }
              }
          }
複製程式碼
  • acquireQueued(final Node node, int arg)

    來到這個方法,證明已經通過tryAcquire獲取同步狀態失敗了,並且呼叫了addWaiter方法將當前執行緒新增至CLH佇列的尾部了,剩下的就是在等待狀態當中等其他執行緒來喚醒自己去獲取同步狀態了。

    對於已經處於CLH佇列當中的執行緒,是以獨佔並且不可中斷的模式去獲取同步狀態。

      final boolean acquireQueued(final Node node, int arg) {
          boolean failed = true;  //成功獲取資源的狀態
          try {
              boolean interrupted = false;  //是否被中斷的狀態
              for (;;) { //自旋--死迴圈
                  final Node p = node.predecessor();  //獲取當前節點的前置節點
                  //如果前置節點是首節點,並且已經成功獲取同步狀態
      if (p == head && tryAcquire(arg)) { 
                      setHead(node);  //設定當前節點為head節點,並且將當前node節點的前置節點置null
                      p.next = null; //設定原head節點的後置節點為null,方便GC回收原來的head節點
      failed = false; 
                      return interrupted; //返回是否被中斷
                  }
                  //獲取同步狀態失敗後,判斷是否需要阻塞或中斷
                  if (shouldParkAfterFailedAcquire(p, node) &&
                      parkAndCheckInterrupt())
                      interrupted = true;  //如果被中斷過,設定標記為true
              }
          } finally {
              if (failed)
                  cancelAcquire(node);  //取消當前節點繼續獲取同步狀態的嘗試
          }
      }
複製程式碼
  • shouldParkAfterFailedAcquire(Node pred, Node node)

    對於獲取狀態失敗的節點,檢查並更新其狀態,如果執行緒阻塞就返回true,這是所有獲取狀態迴圈的訊號控制方法。

    要求pred == node.prev

實際上除非鎖獲取成功,要不然都會被阻塞起來

      private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
          int ws = pred.waitStatus;  //獲取前驅節點的狀態
          //狀態為-1,表示後繼節點已經處於waiting等待狀態,等該節點釋放或取消,就會通知後繼節點
      if (ws == Node.SIGNAL) 
              return true;
          //如果狀態大於0--取消狀態,就跳過該節點迴圈往前找,找到一個非cancel狀態的節點
          if (ws > 0) {
              do {
                  node.prev = pred = pred.prev;
              } while (pred.waitStatus > 0);
              //賦值pred的後繼節點為node節點
              pred.next = node;
          } else {  //如果狀態小於0
              //必須是PROPAGATE或者0--表示無狀態,當是-2的時候,在condition queue佇列當中
              //通過CAS設定pred節點狀態為signal
              compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
          }
          return false;
      }
複製程式碼
  • parkAndCheckInterrupt()

    還有當該節點的前驅節點狀態為signal時,才可以將該節點所線上程pack起來,否則無法將執行緒pack。

      private final boolean parkAndCheckInterrupt() {
          //通過LockSupport工具阻塞當前執行緒
          LockSupport.park(this);
          return Thread.interrupted();  //清除中斷標識,返回清除前的標識
      }
複製程式碼
  • cancelAcquire(Node node)

    該方法是取消節點所線上程對同步狀態的獲取,那說白了就是將節點的狀態改為cancelled.

      private void cancelAcquire(Node node) {
          // Ignore if node doesnt exist
          if (node == null)  //節點為空則返回
              return;
      ​
          node.thread = null;  //節點所線上程設為null
      ​
          // Skip cancelled predecessors
          //獲取node節點的前驅節點
          Node pred = node.prev;
          //迴圈獲取前驅節點的狀態,找到第一個狀態不為cancelled的前驅節點
          while (pred.waitStatus > 0)
              node.prev = pred = pred.prev;
      ​
          // predNext is the apparent node to unsplice. CASes below will
          // fail if not, in which case, we lost race vs another cancel
          // or signal, so no further action is necessary.
          //獲取pred節點的後繼節點
          Node predNext = pred.next;

          //設定node節點狀態為CANCELLED
          node.waitStatus = Node.CANCELLED;
      ​
          //如果node節點是tail節點,通過CAS設定tail節點為pred
          if (node == tail && compareAndSetTail(node, pred)) {
              //通過CAS將pred節點的next節點設定null
              compareAndSetNext(pred, predNext, null);
          } else {  //如果不是tail節點
      ​
              int ws;  //初始化node節點狀態變數

              /*
              *如果pred不是head節點,並且狀態是SIGNAL或者狀態小於0並且設定pred
              *狀態為SIGNAL成功,。並且pred所封裝的執行緒不為空
              */
              if (pred != head &&
                  ((ws = pred.waitStatus) == Node.SIGNAL ||
                   (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                  pred.thread != null) {
                  //獲取node節點的後繼節點
                  Node next = node.next;
                  //如果後繼節點部位null並且狀態不為cancelled
                  if (next != null && next.waitStatus <= 0)
                      //設定pred的後繼節點為next,也就是將pred的後繼節點不再是node
                      compareAndSetNext(pred, predNext, next);
              } else {
                  unparkSuccessor(node);  //釋放後繼節點
              }
      ​
              node.next = node; // help GC
          }
      }
複製程式碼
  • unparkSuccessor(Node node)
      private void unparkSuccessor(Node node) {
          //獲取node節點的狀態
          int ws = node.waitStatus;
          if (ws < 0)  //如果狀態小於0,為SIGNAL -1 或 CONDITION -2 或 PROPAGATE -3
              //通過CAS將node節點狀態設定為0
              compareAndSetWaitStatus(node, ws, 0);
      ​
      //獲取node節點的後繼節點 
          Node s = node.next;
          //如果後繼節點為空或者狀態大於0--cancelled
          if (s == null || s.waitStatus > 0) {
              //後繼節點置為空
              s = null;
              //從tail節點開始往前遍歷
              for (Node t = tail; t != null && t != node; t = t.prev)
                  if (t.waitStatus <= 0)  //判斷狀態小於等於0,就是為了找到狀態不為cancelled的節點
                      s = t;  //找到最前的狀態小於等於0的節點
          }
          if (s != null)  //如果由以上方法找到的節點不為空
              //通過LockSupport工具釋放s節點封裝的執行緒
              LockSupport.unpark(s.thread);
      }
複製程式碼

經過了以上的分析,我想我們對入列的程式碼也有了一個比較好的瞭解吧,那我們也可以嘗試畫一下入列的流程圖。

AQS入列流程.jpg

出列

出列的操作相對於入列來說就真的是簡單的多了,畢竟入列的時候需要考慮的因素太多,要考慮前驅和後繼節點,還要考慮節點的狀態等等一堆因素,而出列就是指CLH佇列的頭部節點,所以麻煩的因素就會少了很多。

release(int arg)

我們廢話都不多說了,直接上程式碼吧。

這也是以獨佔模式來釋放物件

  public final boolean release(int arg) {
      if (tryRelease(arg)) {
          Node h = head;  //獲取head節點
          //如果head節點不為空並且狀態不為0,也就是初始節點
  if (h != null && h.waitStatus != 0) 
              unparkSuccessor(h);  //喚醒後繼節點
          return true;
      }
      return false;
  }
複製程式碼
  • tryRelease(int arg)

    這個方法與入列的tryAcquire一樣,是隻有一個異常的,也就是證明這個方法也是由自定義的同步元件自己去實現,在AQS同步器當中只是定義一個方法而已。

      protected boolean tryRelease(int arg) {
          throw new UnsupportedOperationException();
      }
複製程式碼
  • unparkSuccessor(Node node)

    這個方法實際在入列的時候已經講過了,我直接搬上面的程式碼解釋下來。

      private void unparkSuccessor(Node node) {
          //獲取node節點的狀態
          int ws = node.waitStatus;
          if (ws < 0)  //如果狀態小於0,為SIGNAL -1 或 CONDITION -2 或 PROPAGATE -3
              //通過CAS將node節點狀態設定為0
              compareAndSetWaitStatus(node, ws, 0);
      ​
      //獲取node節點的後繼節點 
          Node s = node.next;
          //如果後繼節點為空或者狀態大於0--cancelled
          if (s == null || s.waitStatus > 0) {
              //後繼節點置為空
              s = null;
              //從tail節點開始往前遍歷
              for (Node t = tail; t != null && t != node; t = t.prev)
                  if (t.waitStatus <= 0)  //判斷狀態小於等於0,就是為了找到狀態不為cancelled的節點
                      s = t;  //找到最前的狀態小於等於0的節點
          }
          if (s != null)  //如果由以上方法找到的節點不為空
              //通過LockSupport工具釋放s節點封裝的執行緒
              LockSupport.unpark(s.thread);
      }
複製程式碼

這上面就是出列也就是釋放的程式碼了,其實看起來不是很難。

小結

花了整整3天左右的時間去看了一下AQS的原始碼,會去看也純屬是想要把自己的併發方面的知識能夠豐富起來,但是這次看原始碼也還是不太順利,因為很多程式碼或者方法,單獨分開來看的時候或許能理解,感覺方法的作用也的確是那麼回事,但是當一整個流程串起來的時候也還是不太明白這樣做的具體作用,以及整個的執行流程。更加沒辦法理解那些自旋里的程式碼,每一次執行會出現怎樣的結果,對CLH佇列的影響。

不過,自己也是有收穫的,至少相較於一開始來說,自己對AQS有了一點皮毛的理解,不至於以後聞起來完完全全是一問三不知的狀態。

同時也希望我這篇文章能夠對想要了解AQS的程式猿能夠起一點作用,以後自己也還是將自己的一些學習心得或者資料共享出來。

參考資料

方騰飛:《Java併發程式設計的藝術》

如需轉載,請務必註明出處,畢竟一塊塊搬磚也不是容易的事情。

相關文章