<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      多線程與高并發(三)—— 源碼解析 AQS 原理

      一、前言

      AQS 是一個同步框架,關于同步在操作系統(一)—— 進程同步 中對進程同步做了些概念性的介紹,我們了解到進程(線程同理,本文基于 JVM 講解,故下文只稱線程)同步的工具有很多:Mutex、Semaphore、Monitor。但是Mutex 和 Semaphore 作為低級通信存在不少缺點,Monitor 機制解決了上面部分缺憾,但是仍然存在問題,AQS 的出現很好的解決了這些問題。

      二、其他同步工具的缺點

      Mutex

      • Mutex 只有兩種狀態,鎖定和非鎖定,無法表示臨界區的可用資源數量(計數信號量可解決)。
      • 使用復雜,使用不當易造成死鎖

      Semaphore

      • Mutex 只允許一個線程訪問臨界資源, Semaphore 允許一定數量的線程訪問共享資源。但是 Semaphore 中沒有 Owner,無法知道當前獲取鎖的線程是誰。
      • 使用復雜,使用不當易造成死鎖,P V 操作需要配對使用,分散在代碼各處增大了編碼難度。

      Monitor

      Monitor 解決了上述幾個問題,但在 HotSpot 中底層是基于 Mutex 做的線程同步,在 1.6 之前且還沒有進行優化,每次鎖競爭都需要經歷兩次上下文切換嚴重影響性能。第二個問題是 HotSpot 實現的是精簡的 Mesa 語義,不支持多個條件變量。

      三、AQS 概述

      什么是 AQS ?

      AQS 即一個類,java.util.concurrent.AbstractQueuedSynchronized.Class,這個類作為一個構造同步器的框架。AQS 本身并不提供 API 給程序員用于直接的同步控制,它是一個抽象類,通過實現它的抽象方法來構建同步工具,如 ReentrantLock、CountDownLatch 等。也就是說它不是一個面向業務開發使用的工具,是構建同步器所復用的一套機制抽取出來形成的框架,這個框架我們自己也可以通過實現它的抽象方法來構建自己的同步器。

      AQS 做了什么事?

      其實通過觀察同步器的各種實現都是相似的,我們會發現鎖的實現通常需要以下三要素:

      • 狀態管理
      • 加鎖解鎖操作
      • 線程等待

      狀態管理在信號量中是整型值,在 Mutex 中就是 Owner,在 Synchronized (Monitor) 中也是 Owner,這些機制都有對應的操作來加鎖和解鎖,通常加鎖/解鎖操作也對應著線程的阻塞(等待)/喚醒,這些線程沒有獲取到鎖后需要等待,有的實現是自旋,有的實現是掛起。不論是 Synchronized 還是 AQS 都有等待隊列供未獲取到鎖的線程進入,直到鎖釋放。
      同理,AQS 所做的主要的三件事也就是上面三件,只是每一項的實現細節可能不同,支持的功能更廣泛。

      • 狀態的原子性管理
      • 加鎖/解鎖操作
      • 隊列管理(線程阻塞入隊/喚醒出隊)

      下文會先列出 AQS 的設計,也就是實現了哪些特性,接下來就會通過看源碼和圖示來了解這些設計的具體實現。

      AQS 設計

      • 阻塞和非阻塞
      • 可選超時設置,在超時后放棄等待鎖
      • 鎖可中斷
      • 獨占和共享模式

      獨占模式即同一時刻只能有一個線程可以通過阻塞點,共享模式下可以同時有多個線程在執行。
      以上都是 AQS 需要支持的功能,基于模板模式的設計,AQS 提供了以下方法供子類繼承重新實現:

      • tryAcquire()/tryRelease() 為獨占模式下的加鎖解鎖操作
      • tryAcquireShared()/tryReleaseShared() 為共享模式下的加鎖解鎖操作
      • isHeldExclusively() 表明鎖是否為獨占模式,即當前線程是否獨占資源。

      四、AQS 原理

      原理概括

      關于同步器的實現思路,首先需要有一個狀態來標明鎖是否可用,在 AQS 的實現中,其維護了一個變量 state,這個變量使用 volatile 關鍵字進行標識,保證其在線程之間的可見性。加鎖解鎖操作簡化來看就是只需要把這個狀態更改,且標明當前線程占有鎖。在獨占模式下,加鎖操作必須互斥,也就是在同一時刻只能有一個線程加鎖成功,AQS 使用 CAS 原子指令來保證 State 的互斥訪問。在一個線程成功加鎖(改變鎖的狀態 State 的值,該值具體如何改變取決于同步工具如何實現)之后,其他線程嘗試 CAS 則會加鎖失敗,那么加鎖失敗的線程該如何呢?這些線程可以自旋等待或者阻塞,AQS 提供 CLH 隊列將這些阻塞的線程管理起來,CLH 是一個先進先出的隊列,加鎖失敗的線程會被進入隊列阻塞。因此加鎖和解鎖操作并不僅僅是簡單的修改鎖狀態,在這之后還需要維護隊列。AQS 的主要原理也就是圍繞著隊列進行入,加鎖解鎖功能由繼承 AQS 的同步工具實現,在調用加鎖操作之后,AQS 來維護線程入隊,并且將線程阻塞,在調用解鎖操作后,AQS 將隊列中的線程按規則出隊并且喚醒。

      Node 設計

      static final class Node {
      
              static final Node SHARED = new Node();  // 共享模式下的節點
      
              static final Node EXCLUSIVE = null; // 獨占模式下的節點
      
              static final int CANCELLED =  1; // 被取消了的狀態
       
              static final int SIGNAL    = -1; // 處于park() 狀態等待被unpark()喚醒的狀態
      
              static final int CONDITION = -2; // 處于等待 condition 的狀態
         
              static final int PROPAGATE = -3; // 共享模式下需要繼續傳播的狀態
      
              volatile int waitStatus; // 當前節點的狀態
      
              volatile Node prev; // 前驅節點指針
      
              volatile Node next; // 后繼節點指針
      
              volatile Thread thread; // 搶占鎖的線程
      
              Node nextWaiter; // 指向下一個也處于等待的節點
          }
      

      上面提到 AQS 有一個阻塞隊列,這個隊列的具體實現是雙向鏈表,隊列中的節點是對線程進行封裝后的 Node 類,這個類的每個字段含義如下:

      1. 節點狀態,即節點的 waitStatus 值
      • CANCELLED 當前節點因為超時或者被打斷而取消,處于這個狀態的節點永遠不會被阻塞
      • SIGNAL 當前節點的前驅節點被阻塞或者即將被阻塞,所以當前節點必須在釋放或者取消的時候對其unpark()
      • CONDITION 當前節點處于 Condition 隊列等待,Condition 隊列不是 AQS 中的 CLH 隊列,這個狀態不是用于 CLH 隊列的,只有當該節點從 Condition 轉移到 CLH 中時,這個狀態才會生效(詳細內容見下一篇章 ReentrantLock)。
      • PROPAGATE 共享狀態下對鎖釋放的時候需要釋放所有獲取鎖的節點,因而需要這個狀態表明還需要繼續釋放
      • 0 這個值也是waitStatus 的值,沒有進行常量定義,其含義為不是以上值
      1. waitStatus 即當前線程現在的狀態,值就是上面列出的這些
      2. nextWaiter 是 Condition 中指出下一個也處于等待隊列的節點

      五、源碼分析

      為了更好的講解原理,下面直接模擬線程競爭鎖的場景來進行分析,我們首先看兩個線程在獨占鎖模式下加鎖和解鎖的源碼,再看共享模式下的場景。

      獨占模式加鎖

      場景:線程 T1 和線程 T2 競爭鎖

      1. acquire(int arg)

      acquire(int arg) 是獨占模式下加鎖的入口方法

      public final void acquire(int arg) {
              // tryAcquire(arg) 是繼承 AQS 實現在獨占模式下的同步工具需要重寫的方法。當這個方法返回true,就表示當前線程獲得了鎖。
              if (!tryAcquire(arg) &&
                  acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                  selfInterrupt();
          }
      

      流程如下:

      1. 線程 T1 和線程 T2 同時要執行臨界區,在執行臨界區之前需要進行加鎖操作,也就是同步工具實現的加鎖方法,加鎖方法最終會走到 AQS 的 acquire(int arg) 方法;
      2. 此時,兩個線程同時首先都執行 tryAcquire(arg) 操作,該方法在重寫的時候通過 CAS 設置狀態 State 的值,CAS 保證只能有一個線程成功執行,即只有一個線程能加鎖成功,假設線程 T1 加鎖成功。
      3. 線程 T1 執行 tryAcquire(arg) 加鎖成功,!tryAcquire(arg) 取反操作后 T1 線程不必執行后續邏輯,即加鎖成功,開始執行臨界區
      4. 此時線程 T2 執行 tryAcquire(arg) 必然失敗 (由于 State 的值被修改過,執行 CAS 失敗即加鎖不成功)
      5. tryAcquire(arg) 返回false , 取反后為 true ,執行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
      6. acquireQueued() 的結果方法分四種情況(具體情況見這個方法的解析),總之最后會返回一個標記表明當前線程是否需要被打斷,如果為true,則調用 selfInterrupt() 再次設置打斷標記
      static void selfInterrupt() {
              Thread.currentThread().interrupt();
          }
      

      2. addWaiter(Node mode)

      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
              /**
               * 1. 將尾部賦給一個臨時變量,注意此時tail為空,因為tail此時還沒有指向Node對象
               * 2. tail == null,因此執行 enq(node) 方法,這個node是創建的當前線程的node
               */
              Node pred = tail;
              if (pred != null) {
                  node.prev = pred;
                  // 這個CAS方法的含義是,判斷當前AQS的尾部節點是pred,如果是則重新設置為當前node
                  // 設定成功,則退出
                  if (compareAndSetTail(pred, node)) {
                      pred.next = node;
                      return node;
                  }
              }
              // 如果掛載隊尾節點也存在競爭 則使用無限CAS自旋方式設置隊尾
              enq(node);
              return node;
          }
      

      對上述流程第4點拆解來看,T2 首先要執行addWaiter(Node.EXCLUSIVE)進行入隊,我們看下執行流程:

      1. 根據當前線程實例化一個獨占式的 Node
      2. 將尾部賦給一個臨時變量,看官方注釋,該指針是懶加載的,在執行enq()方法新入一個節點的時候 tail 才有值,所以此時為 NULL
      3. tail == null,因此執行 enq(node) 方法進行當前節點入隊操作,入隊成功后返回該節點。
        當隊列中的節點大于一個時,也就是有實際節點的時候,tail 指針是不為空的,那么在調用 enq 入隊之前會嘗試一次快速入隊,如果失敗表明入隊也有競爭,才會進入 enq() 方法

      3. enq(final Node node)

      private Node enq(final Node node) {
              // 死循環判斷
              for (;;) {
                  /**
                   * 第一次循環:
                   *  tail賦值給臨時變量t, 注意此時tail仍然是null, 進入if塊
                   * 調用compareAndSetHead(new Node()) 創建了一個新的節點 ,這個節點的結構如下:
                   *         Node() {    // Used to establish initial head or SHARED marker
                   *         }
                   *   該節點中的線程為 NULL,也就是我們下文所稱的啞節點
                   *   此時隊列里有了第一個Node
                   */
                  Node t = tail;
                  if (t == null) { // Must initialize
                      if (compareAndSetHead(new Node()))
                          tail = head;
                  } else {
                      /**
                       * 第二次循環:在第一次循環時head賦給了tail,此時tail 不為空,
                       * 進入else塊,將線程T2入隊,也就是維護鏈表關系
                       */
                      node.prev = t;  // 操作1
                      if (compareAndSetTail(t, node)) { // 操作2
                          t.next = node; // 操作3
                          return t;
                      }
                  }
              }
          }
      

      enq() 算是上述整個流程的一個分支,為后續講解更清楚,有必要先詳細講解此方法,畫出入隊情形下的隊列圖,流程如下:

      1. 注意這是一個 for 循環,在第一次執行的時候,由于 tail 為 NULL,因此進入 if 塊創建了一個空節點,空節點的意思是該節點中沒有包含線程,如圖所示。

      1. 第二次循環時,tail 即指向沒有線程的那個Node,以下稱啞節點,此時 tail 不為空,進入 else 邏輯。
      2. 首先更新 T2 Node 的 prev 指針指向啞節點,然后通過 CAS 設置 T2 Node 為尾節點,因為當前尾節點指向的就是啞節點 t,因此 compareAndSetTail(t, node) 可以成功執行,執行成功后,尾節點指針指向 T2 Node。
      3. t.next = node 將啞節點的 next 指針指向 T2。

      下圖是 T2 入隊成功后隊列圖:

      上面是在入隊時沒有競爭的流程,我們看假如此時 T3 線程也在執行入隊邏輯會怎么樣。
      如代碼中所標示,三個操作并不是原子性的。如下圖所示,對上述隊列圖做了簡化。
      隊列初始完畢時 head 和 tail 指向 Dummy 節點,如果此時 T2 線程和 T3 線程同時執行到 node.prev = t 邏輯

      接下來執行 compareAndSetTail(t, node) 設置尾節點,由于是 CAS 操作,因此只有一個線程能成功,假設 T2 此次設置成功了,此時隊列圖如下。

      T3 線程執行 CAS 則失敗,因為是 for 循環,所以重新執行一遍 for 中的邏輯,此時 T3 拿到的尾節點就是 T2了,這次沒有競爭的情況下將尾節點設置為了自己并且 prev 指向 T2, 此時隊列圖如下。

      注意此時的圖,此時兩個線程都還未執行 t.next = node,兩個 next 指針都還沒有被設置,實際情況可能 T2 或者 T3 先執行了這兩個語句,但是無論如何 next 指針會出現為 null 的情況,這種情況涉及到兩個問題,一是從隊列中出隊的時候如果用 next 指針從前往后遍歷取節點的時候會誤以為沒有下一個節點實際上有,二是在 ReentrantLock 判斷線程是否需要排隊時特殊處理(留待 ReentrantLock 細講)。
      當執行完這兩條語句后,入隊就結束了,可以看到整個入隊過程是可以保證最終結果是正確的。

      4. acquireQueued(final Node node, int arg)

      對上述流程第4點進行拆解,執行完畢 addWaiter() 返回當前節點 T2 Node,接著就進入acquireQueued 方法,我們來看該方法的流程:

      final boolean acquireQueued(final Node node, int arg) {
              // 標記自己是否拿到了資源 true為沒有獲取到。
              boolean failed = true;
              try {
                  // 標記等待過程中是否被中斷過
                  boolean interrupted = false;
                  for (;;) {
                      // 拿到前驅節點
                      final Node p = node.predecessor();
                      // 判斷上一個節點是否是頭部,即當前節點為隊列中第二個節點,tryAcquire()嘗試獲取鎖
                      if (p == head && tryAcquire(arg)) {
                          // 將自己設置為頭結點
                          setHead(node);
                          // 斷開原先的啞節點與當前節點的next連接
                          p.next = null; // help GC
                          // 標記已經獲取到節點
                          failed = false;
                          // 返回線程中斷標記
                          return interrupted;
                      }
                      /**
                       *  判斷在一次自旋加鎖失敗后是否需要睡眠
                       * 	自旋第一次時shouldParkAfterFailedAcquire(p, node)返回false, 不會進入if塊
                       * 	自旋第二次時,shouldParkAfterFailedAcquire(p, node)返回true,
                       * 	此時進入parkAndCheckInterrupt()方法,此時線程阻塞在 parkAndCheckInterrupt()
                       * 	如果線程休息過程中被中斷過(Thread.interrupted();), interrupted 返回true
                       */
                      if (shouldParkAfterFailedAcquire(p, node) &&
                              parkAndCheckInterrupt())
                          interrupted = true;
                  }
              } finally {
                  if (failed)
                      cancelAcquire(node);
              }
          }
      

      該方法有兩個標記,一個是 failed 標記是否拿到了資源,注意默認為 true 的時候是沒有拿到,另一個是中斷標記 interrupted 默認為 false, 該標記的含義不是該線程是否已經被打斷過,而是是否需要被打斷。我們先看 for 循環中的邏輯再看 finally 塊。

      1. 首先拿到當前節點的前驅節點,當前節點就是返回的節點 T2 Node,因此它的前驅節點就是啞節點。
      2. 判斷上一個節點是否為頭部,如果是通過 tryAcquire(arg) 再次嘗試獲取鎖。CLH 是一個先進先出的隊列,第一個節點是啞節點,所以無需競爭鎖,也就是只有隊列中的第二個節點才有再次競爭鎖資源的資格。這里為什么又要調用一次 tryAcquire() 呢?因為這時候線程 T1 有可能已經釋放了資源,這里也就是常說的自旋鎖,但是只自旋了一次。
      3. 如果這次加鎖成功,那么進入 if 塊,首先看 setHead()的邏輯,在這里 T2 節點替代了原先的啞節點作為頭節點,并且自己成了啞節點。然后斷開與之前的啞節點的所有連接,原啞節點等待 GC 回收。
      private void setHead(Node node) {
              head = node;
              // 當前節點的線程字段置為空,也就是這個節點替代了原先的啞節點變成了新的啞節點
              node.thread = null;
              // 斷開與原啞節點的prev連接
              node.prev = null;
          }
      

      1. failed 標記設置為 false,表示成功獲取到資源,然后返回 interrupted 中斷標記,此時還是為 false ,也就是無需打斷,方法返回之后就可以開始執行臨界區了。關于 interrupted 標記待 parkAndCheckInterrupt 方法的時候會細講,在這次如果直接加鎖成功的情況下也就是還沒有走下面的 park 阻塞邏輯的時候,該打斷標記就是 false。
      2. 如果此次加鎖失敗,則向下執行,首先執行 shouldParkAfterFailedAcquire(p, node) 方法

      5. shouldParkAfterFailedAcquire(Node pred, Node node)

      /**
           * 檢查當前節點并更新狀態,返回是否需要被阻塞
           * Checks and updates status for a node that failed to acquire.
           * Returns true if thread should block. This is the main signal
           * control in all acquire loops.  Requires that pred == node.prev.
           *
           * @param pred node's predecessor holding status 前置節點
           * @param node the node 當前節點
           * @return {@code true} if thread should block 是否需要被阻塞
           */
          private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
              // 前置節點的狀態,ws默認值為0
              int ws = pred.waitStatus;
              // SIGNAL默認值為1
              if (ws == Node.SIGNAL)
                  /*
                   * This node has already set status asking a release
                   * to signal it, so it can safely park.
                   */
                  return true;
              // 前置節點處于取消狀態
              if (ws > 0) {
                  /*
                   * Predecessor was cancelled. Skip over predecessors and
                   * indicate retry.
                   * 如果前置節點不是正常的等待狀態那么就繼續往前找直到找到一個正在等待裝填的節點。將其后置節點斷開接上當前節點。GC會回收一堆相互引用又沒有外部引用的節點。
                   */
                  do {
                      node.prev = pred = pred.prev;
                  } while (pred.waitStatus > 0);
                  pred.next = node;
              } else {
                  /*
                   * waitStatus must be 0 or PROPAGATE.  Indicate that we
                   * need a signal, but don't park yet.  Caller will need to
                   * retry to make sure it cannot acquire before parking.
                   */
                  compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
              }
              return false;
          }
      

      接上述流程,T2 線程獲取資源失敗,因而走此方法邏輯,此時傳進該方法的參數 p 是 T2 Node 的前置節點,也就是啞節點(注意因沒獲取到資源沒有進入if塊,啞節點還是最先創建的那個啞節點)。這個方法就是在線程沒有獲取資源的情況下不斷自旋直到進入阻塞狀態或者是加鎖成功,shouldParkAfterFailedAcquire 就是返回一個布爾值判斷當前線程是否需要進行阻塞。

      流程如下:

      1. 拿到啞節點的 waitStatus (以下簡稱ws)狀態,這個狀態初始值是 0。
      2. ws 的值為 0, 所以進入 else 邏輯,將 ws 設置為了 -1 (SIGNAL),返回flase。
      3. 回到 acquireQueued() 方法 因為返回的是 false,&& 的短路性質,沒有再執行后一個邏輯,進入下一次循環
      4. 假設 T2 此時仍然沒有加鎖成功,繼續走到這個 shouldParkAfterFailedAcquire() 這個方法中,由于上一次啞節點的狀態被修改成了 SIGNAL,這次進入第一個 if 塊,返回了 true。
      5. 回到 acquireQueued() 方法,向下執行 parkAndCheckInterrupt(),我們接著看該方法的邏輯。

      6.parkAndCheckInterrupt()

      private final boolean parkAndCheckInterrupt() {
              // 線程阻塞在這,被打斷后才會往下執行, 此時打斷標記為 true
              LockSupport.park(this);
              // 返回打斷標記并重置為false
              return Thread.interrupted();
          }
      

      這個方法比較簡單,就是在上一個方法判斷要阻塞之后調用這個方法,通過執行 park() 方法將線程阻塞在此處,直到有另一個線程將其喚醒。喚醒的方式有兩種,一是調用 unpark() 方法,二是使用 interrupt() 方法置中斷標記為 true。T2 Node 阻塞在此處被喚醒后就繼續往下走執行 Thread.interrupted(), 該方法會重置中斷標記并且給出是否被中斷,因此此方法最終會返回一個 bool 值,表明該線程是否需要被打斷。
      T2 Node 執行到此處則一直阻塞在這了,為了講清楚這些細節,我們按實際場景講述,這個線程就阻塞在這了, T2 Node 被喚醒必然是另一個線程對其執行了 unpark() 或者 interrupt() 方法,我們先來看調用 unpark() 的情況,也就是線程 T1 解鎖的流程。

      獨占模式解鎖

      1. release(int arg)

      書接上回,T2 阻塞住了,此時 T1 執行完畢臨界區,開始進行解鎖,解鎖操作首先是恢復鎖狀態 State 為可用,其中是否重入等細節由繼承AQS 的同步工具實現(待講 ReentrantLock 再講此處細節),此處略過不表,我們重點關注 AQS 的鎖釋放原理,解鎖過程(獨占模式下)最終會調用到 AQS 的 release(int arg) 方法,流程如下:

      public final boolean release(int arg) {
              if (tryRelease(arg)) {
                  // 頭結點存在 且頭結點狀態不為0
                  // 如果頭結點不為初始化狀態則喚醒隊列下一個等待的線程
                  Node h = head;
                  if (h != null && h.waitStatus != 0)
                      unparkSuccessor(h);
                  return true;
              }
              return false;
          }
      
      1. 首先調用 tryRelease() 方法,和加鎖同理,這個方法也由繼承 AQS 的同步工具釋放,這個函數返回一個布爾值,true 表明釋放鎖成功,false 解鎖失敗,T1 線程釋放資源后,這個方法必然返回true,于是進入 if 塊。
      2. 判斷頭節點是否存在且頭節點狀態是否不為0,我把上面 T2 入隊的圖拿了下來,可以看到這個時候頭節點是啞節點,其狀態 ws 在 T2 入隊的時候被修改了為 -1,此時可以進入if塊,執行unparkSuccessor() 方法,入參是啞節點,我們接下來看這個方法的源碼。

      2. unparkSuccessor(Node node)

          private void unparkSuccessor(Node node) {
              /*
               * If status is negative (i.e., possibly needing signal) try
               * to clear in anticipation of signalling.  It is OK if this
               * fails or if status is changed by waiting thread.
               * 獲取當前節點狀態
               */
              int ws = node.waitStatus;
              // 該節點狀態是有效的 就將其設置為初始化狀態
              if (ws < 0)
                  compareAndSetWaitStatus(node, ws, 0);
      
              /*
               * Thread to unpark is held in successor, which is normally
               * just the next node.  But if cancelled or apparently null,
               * traverse backwards from tail to find the actual
               * non-cancelled successor.
               * 從尾節點往前找, 找到一個waitStatus 小于0 的Node
               */
              Node s = node.next;
              if (s == null || s.waitStatus > 0) {
                  s = null;
                  for (Node t = tail; t != null && t != node; t = t.prev)
                      if (t.waitStatus <= 0)
                          s = t;
              }
              if (s != null)
                  LockSupport.unpark(s.thread);
          }
      

      此時流程如下:

      1. 取出啞節點狀態,此時為-1,將其更改為0;
      2. 接著取出下一個節點,在這里啞節點的下一個節點即 T2 Node,T2 不為空,執行 LockSupport.unpark(s.thread) 喚醒 T2 線程。
        注意這里有一個 s== null 的 if 分支,這是因為 next 指針是不可靠的,上面 enq() 方法已經細講過 ,會出現為 null 的情況,因此需要通過 prev 指針來從尾部開始遍歷直到找到最后個可被喚醒的線程,注意這里說的最后一個的意思即是隊頭開始第一個需要被喚醒的線程(沒有調用 break 打破循環),因此并沒有破壞隊列先進先出的規則。

      T2 線程被 unpark() 喚醒

      上面的解鎖流程的最后,T2 線程被喚醒,繼續往下執行, 注意這里我看其他博客說的有點問題,在被 unpark 喚醒的情況下是不會修改中斷標記的,因此這里返回的是 false,所以回到 acquireQueued 方法中并不會執行下面這行代碼,關于 unpark() 和 interrupt() 我單獨開篇博客講下

      T2 線程被喚醒后就開始有了競爭鎖的資格,這時候 for 循環又開始執行,即再次競爭鎖,假如這次加鎖成功(加鎖失敗就又繼續阻塞,咱就沒必要繼續講了,為了閉環我們假設這次加鎖成功),那么返回中斷標記 interrupted,這個值為 false。

      此時也就完成了鎖的更替 由 T1 變成了 T2,整個環節就閉環了,在這上面的講解過程中沒有引入更多的線程是為了避免討論復雜化,在 T3、T4 甚至更多線程來臨時同樣是遵循的上述代碼流程,只是因為多線程并發,這其中哪個線程能獲取到鎖、鏈表的維護是否正確,喚醒的時候喚醒的是哪個線程這些事并不可控,因而也沒有必要一個個場景來復現,只要弄清楚原理想必足夠了。

      T2 線程被 interrupt() 喚醒

      1. 這個流程和上述 unpark() 流程是并列的,承接的是 T2 被阻塞住的流程,T2 阻塞在此處,若 T1 在執行臨界區的時候調用了 T2.interrupt() 方法,則也會對 park 解鎖,T2 線程就被喚醒了,且中斷標記在 interrupt() 執行的時候被置為 true,此方法返回true。
      2. 回到 acquireQueued 方法中,interrupted 標記被修改,但是沒有立即返回,直到 T2 線程獲取到了資源后返回中斷標記 true。
      3. 最終執行到 selfInterrupt() 方法重置一次中斷標記

      一些 AQS 設計的疑問

      1. 在被中斷打斷后為什么還要調用 selfInterrupt() 再中斷一次?

        在 parkAndCheckInterrupt() 方法中,如果線程是被 interrupt() 喚醒的,接下來 Thread.interrupted() 這句將標記清空了,如果調用加鎖的線程要使用該中斷標記,標記卻已經被清除了,因此最后還需要調用一次 selfInterrupt() 設置中斷標記,這也是中斷機制慣例用法。
      2. 在加鎖成功后的出隊過程中,為什么要將出對的線程 Node 去替代啞節點成為新的啞節點?直接斷開指針,啞節點指向下一個節點會如何呢?
      3. 為什么要設置啞節點?(在 ReentrantLock 篇講解)

      結語

      研讀源碼過程中發現所需時間耗費過長,在這個以敏捷為王的時代,偶爾也疑慮如此費勁周章地研習源碼能有何收獲,成本與收獲似不成正比。故而 AQS 的源碼暫且看到這,想必知曉原理已足夠,后續若有閑思再補充共享式加鎖和解鎖流程細節。
      2022.8.10 在看 ReentrantLock 源碼時對之前的一些疑問有了想法,因此對文中 enq() 方法做了補充,勘誤了其中一些錯誤,本文中一些沒有解釋清楚的疑問可以ReentrantLock 源碼篇

      posted @ 2022-07-28 09:41  onAcorner  閱讀(710)  評論(1)    收藏  舉報
      主站蜘蛛池模板: 日本边添边摸边做边爱| 精品国产一区二区三区香| 久久综合九色综合97欧美| 特黄特色的大片观看免费视频| 亚洲曰韩欧美在线看片| 日本中文字幕一区二区三| 中文字幕在线精品视频入口一区| 亚洲高清免费在线观看| 亚洲精品综合网二三区| 久久免费精品国自产拍网站| 久久精品国产清自在天天线| AV最新高清无码专区| 国产精品无码a∨麻豆| WWW丫丫国产成人精品| 成人精品一区二区三区四| 国产在线一区二区不卡| 日本黄漫动漫在线观看视频| 国产精品视频露脸| 中文字幕日韩有码国产| 大桥未久亚洲无av码在线| 少妇无码av无码专区| 国产一区二区不卡自拍| 精品国产免费一区二区三区香蕉 | 日韩视频中文字幕精品偷拍| 午夜三级成人在线观看| 做暖暖视频在线看片免费| 国产成人综合色就色综合| 这里只有精品在线播放| 精品久久久久久成人AV| 日夜啪啪一区二区三区| 国产精品午夜无码AV天美传媒| 亚洲av色香蕉一区二区| 无码欧亚熟妇人妻AV在线外遇| 最新亚洲av日韩av二区| 熟女一区| 国产乱子伦一区二区三区视频播放| 国产va免费精品观看精品| 亚洲精品一区国产精品| 中文字幕一区日韩精品| 伊人久久久av老熟妇色| 护士张开腿被奷日出白浆|