Java並發之AQS同步器學習

來源:互聯網
上載者:User

標籤:aqs   架構   ast   void   current   elf   部分   try   cat   

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 successor‘s 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)  //為空白則拋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節點的原始碼進解說,我想對於之後的內容會有很大的協助的,因為後面的方法當中會有特別多的狀態判斷。

當我們重寫同步器的時候,需要使用同步器的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 doesn‘t 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);      }

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

出列

出列的操作相對於入列來說就真的是簡單的多了,畢竟入列的時候需要考慮的因素太多,要考慮前驅和後繼節點,還要考慮節點的狀態等等一堆因素,而出列就是指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並發編程的藝術》

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

Java並發之AQS同步器學習

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.