從 LockSupport 到 AQS 的簡單學習

學習 AQS 之前, 需要對以下幾點內容都有所瞭解. 本章內容將先從以下幾點開始然後逐步到 AQS.

一. CAS

關於 CAS 在前面文章有寫過, 傳送門 java 基礎回顧 - 基於 CAS 實現原子操作的基本理解


 

二. LockSupport

2.1 傳統線程等待/喚醒機制

日常我們經常使用的等待與喚醒機制無非就是下面兩種方式

  • Object 類中的 waitnotify 方法實現線程等待和喚醒.
  • Condition 接口中的 awaitsignal 方法實現線程的等待和喚醒.

這兩種方式如果大家都使用過的話, 就會知道會有以下兩個問題. 也就是傳統的 synchronizedLock 實現等待喚醒通知的約束

  • 線程先要獲得並持有鎖, 必須在鎖塊(synchronized 或 lock)中.
  • 必須要先等待後喚醒, 線程才能夠被喚醒

LockSupport 就解決了傳統的等待/喚醒機制的痛點, 下面一起來看下 LockSupport 究竟是什麼.

2.2 LockSupport 是什麼

LockSupport 是一個線程阻塞工具類, 內部所有方法都是靜態的, 可以讓線程在任意位置阻塞, 阻塞之後也有對應的喚醒方法. 可以喚醒任意指定線程.
LockSupport 使用了一種名爲 permit(許可)的概念來做到阻塞和喚醒線程的功能, 每個線程都有一個 permit(許可).
permit(許可) 只有兩個值: 0 與 1. 默認爲 0.
 

2.3 LockSupport 的阻塞方法
 public static void park() {
    UNSAFE.park(false, 0L);
 }

permit(許可) 默認是 0, 所以一開始調用阻塞方法 park(), 當前線程就會阻塞, 直到別的線程將當前線程的 permit(許可)設置爲 1 時纔會被喚醒, 被喚醒後會將 permit(許可) 再次設置爲 0 並返回. 別的線程調用了 interrupt 中斷該線程, 也會立即返回.
 

2.4 LockSupport 的喚醒方法
 public static void unpark(Thread thread) {
     if (thread != null)
         UNSAFE.unpark(thread);
 }

在調用 unpark(thread) 方法後, 就會將傳入 thread 線程的 permit(許可) 設置爲 1, 並自動喚醒傳入的 thread 線程, 即之前阻塞中的 LockSupport.park() 方法會立即返回. 多次調用 unpark() 方法並不會累加, 值還會是 1.
 

2.5 LockSupport 簡單例子


這是一個典型的先阻塞後喚醒的例子: thread1 進入後調用 LockSupport.park() 發現 permit(許可) 值爲 0, 直接進入阻塞狀態. 3 秒後進入到 thread2 調用 LockSupport.unpark(thread1)thread1 中的 permit(許可) 值改爲 1, 並喚醒 thread1. 最後在 thread1LockSupport.park() 就會立刻返回, 結束阻塞狀態同時將 permit(許可) 值再改爲 0.

如果讓 thread2 先給 thread1 發放許可, 然後 thread1 再進行阻塞. 又會輸出什麼呢.

輸出結果是沒問題的, 不會被阻塞. 這也就解決了傳統的等待/喚醒機制的痛點.

 

2.6 LockSupport 總結
  • LockSupport 和每個使用它的線程都有一個 (permit)許可 關聯. permit 相當於1,0的開關, 默認是0.
  • 如果將 (permit)許可 看成憑證可能會更容易理解一點. 線程阻塞需要消耗憑證 (permit), 這個憑證最多隻有1個. 默認是沒有憑證的.也就是 0.
  • 調用 park 方法時候, 會先判斷有沒有憑證, 也就是 permit 值是否爲 1.
    • 如果有憑證, 則會直接消耗掉這個憑證然後正常退出. 也就是將 permit的值改爲 0, 然後直接返回.不會阻塞. 這就是爲什麼 LockSupport 可以先喚醒後阻塞的原理.
    • 如果沒有憑證, 就必須阻塞等待有憑證可用. 也就是等待 permit 的值由 0 變爲 1. 阻塞後, 一旦在外部爲此線程發放了憑證(外部調用了 unpark 方法, 將 permit 改爲 1), 那麼將結束阻塞狀態, 並消費掉這個憑證.(將 permit 改爲 0).
  • 調用 unpark 方法, 會爲指定線程發放一個憑證, 將指定線程的 permit 值改 爲 1. 但憑證最多隻能有1個, 累加無效.

問題: 如果先喚醒兩次再阻塞兩次, 最終結果是什麼呢? 答案是最終還是會阻塞線程, 因爲憑證的數量最多就是 1, 喚醒再多次也沒用, 但是隻要阻塞一次, 就會消費掉這個憑證, 再進行阻塞, 就沒有憑證了, 所以還會阻塞.

如果有興趣研究 LockSupport 源碼的同學可以看下這篇文章, 【細談Java併發】談談LockSupport.


 

三. CLH 隊列鎖的概念

CLH 名字的由來即 Craig, Landin, Hagersten 國外三個大牛名字的縮寫.
CLH 隊列是單向隊列, 其主要特點是自旋檢查前驅節點的 locked 狀態
CLH 隊列鎖一個自旋鎖. 能確保無飢餓性. 提供先來先服務的公平性.
CLH 隊列鎖也是一種基於鏈表的可擴展, 高性能, 公平的自旋鎖. 申請鎖線程僅在本地變量上自旋, 它不斷輪詢前面一個節點的狀態, 假設發現上一個節點釋放了鎖就結束自旋.

3.1 CLH 隊列鎖的獲取

CLH 隊列中當一個線程需要獲取鎖的時候, 會將其封裝成爲一個 QNode 節點, 將其中的 locked 設置爲 true, 表示需要獲取鎖, myPred 則表示對其前驅節點的引用.


 
線程 A 要獲取鎖的時候, 會先使自己成爲隊列的尾部, 同時獲取一個指向其前驅節點的引用 myPred
線程 B 想要獲取鎖, 同樣需要放到隊列的尾部, 並將 myPred 指向線程 A 的節點.


 
每個線程就在前驅節點的 locked 字段上自旋, 直到前驅節點釋放鎖. 也就是前驅節點的 locked 字段變爲 false
 
當一個線程需要釋放鎖的時候, 將當前節點的 locked 設置爲 false, 同時回收前驅節點. 如下圖所示. 前驅節點釋放鎖, 線程 A 的 myPred 所指向前驅節點的 locked 字段變爲 false, 線程 A 就可以獲取到鎖.


 

四. AQS 概念

4.1 AQS 簡介

AQS 即 AbstractQueuedSynchronizer 抽象的隊列式同步器. 是用來構建鎖或者其他同步組件的基礎框架. 它不能被實例化, 設計之初就是爲了讓子類通過繼承 AQS 並實現它的抽象方法來管理同步狀態.

簡單理解就是: 如果被請求的共享資源空閒, 則將當前請求資源的線程設置爲有效的工作線程, 並將共享資源設置爲鎖定狀態, 如果被請求的共享資源被佔用, 那麼就需要一套線程阻塞等待以及喚醒時鎖分配的機制, 這個機制就是 AQS, 是基於 CLH 隊列的變體實現的. 它將每一條請求共享資源的線程封裝成隊列的一個節點 Node, 將暫時獲取不到鎖的節點Node加入到隊列中. 通過 CAS, 自旋, 以及 LockSupport.park() 的方式維護內部的一個使用 volatile 修飾的 int 類型共享變量 state 的狀態. 使併發達到同步的效果.

state 變量可以理解爲共享資源, state 的訪問方式有以下三種, 根據修飾符protected final, 說明它們是不可以被子類重寫的, 但是可以在子類中進行調用, 這也就意味這之類可以根據自己的邏輯來決定如何使用 state 的值.

  • getState() : 獲取當前同步狀態
  • setState(int newState) : 設置當前同步狀態
  • compareAndSetState(int expect, int update) : 使用 CAS 設置當前狀態, 該方法可以保證狀態設置的原子性.

ReentrantLock | CountDownLatch | ReentrantReadWriteLock | Semaphore 這些都是基於 AQS 實現的. AQS 的子類應被定義爲內部類, 作爲內部的 helper 對象. 如 ReentrantLock, 它便是通過內部的 Sync 對象來繼承 AQS 的.

CLH 是單向隊列, 這裏說的基於 CLH 的變體是基於鏈表實現的雙向同步隊列, 在 CLH 的基礎上進行了變種. CLH 的特點是自旋檢查前驅節點的 locked 狀態. 而 AQS 同步隊列是雙向隊列, 每個節點也有狀態 waitStatus, 而其也不是一直對前驅節點的狀態自旋, 而是自旋一段時間後阻塞讓出 CPU 時間片, 等待前驅節點主動喚醒後繼節點.

state = 0,代表沒有線程持有鎖,state > 0 有線程持有鎖,並且 state 的大小是重入的次數

4.2 AQS 同步器與鎖之間的關係

AQS 同步器是實現鎖或者任意同步組件的關鍵, 在鎖的實現中聚合同步器. 可以這樣理解二者之間的關係.

  • 鎖是面向使用者的, 它定義了使用者與鎖交互的接口, 隱藏了實現的細節.
  • 同步器是面向鎖的實現者, 它簡化了鎖的實現方式, 平布了同步狀態的管理, 線程的排隊, 等待與喚醒等底層操作.
     
4.3 資源的獲取與釋放

在 AQS 中定義了兩種資源的共享方式.

  • 獨佔式 Exclusive : 只有一個線程能執行, 例如 ReentrantLock
  • 共享式 Share : 多個線程可以同時執行, 如 Semaphore, CountDownLatch, ReadWriteLock.

不同的自定義同步器爭用共享資源的方式也不同, 自定義同步器在實現時只需要實現共享資源 state 的獲取與釋放即可, 至於具體等待隊列的維護, AQS 已經在頂層實現好了. 自定義同步器時主要實現以下幾個方法.

  • isHeldExclusively() : 當前線程是否正在獨佔資源.
  • tryAcquire(int arg) : 獨佔式. 嘗試獲取資源. 成功返回 true, 失敗返回 false.
  • tryRelease(int) : 獨佔式. 嘗試釋放資源. 成功返回 true, 失敗返回 false.
  • tryAcquireShared(int) : 共享式. 嘗試獲取資源. 返回大於等於 0 的值表示成功, 反之獲取失敗.
  • tryReleaseShared(int) : 共享式. 嘗試釋放資源. 如果釋放後允許喚醒後續等待節點返回 true, 否則返回 false.

ReentrantLock 爲例. state 的初始值爲 0, 表示未鎖定狀態. 當 A 線程 Lock() 時, 會調用 tryAcquire(int arg) 方法獨佔該鎖並將 state + 1. 後面其他線程再調用 tryAcquire(int arg) 時就會失敗, 直到 A 線程調用了 unLock() 後將 state = 0 釋放鎖爲止, 其他線程纔會有機會獲取到鎖. A 線程在釋放之前, 是可以自己重複獲取次鎖的, 即將 state 累加. 最後釋放的時候, 重入幾次鎖就需要釋放幾次鎖, 這樣才能保證 state 的值變爲 0. 這便是鎖的可重入概念.

一般來說, 我們自定義同步器時, 要麼是獨佔的方式, 要麼是共享的方式. 但是 AQS 也支持同時實現獨佔和共享兩種方式. 例如 ReentrantReadWriteLock

 

4.4 AQS 中的 Node 節點

上面說過了, 在 AQS 中將請求共享資源的線程都封裝成爲了一個 Node 節點. 現在我們來看一下這個 Node 裏面都包含了什麼.
AbstractQueuedSynchronizer.java 380 行

   static final class Node {  
           //表示線程以共享的模式等待鎖
           static final Node SHARED = new Node();
           //表示線程以獨佔的模式等待鎖
           static final Node EXCLUSIVE = null;

           //節點狀態值----表示線程獲取鎖的請求已取消. 當timeout或被中斷(響應中斷的情況下),會觸發變更爲此狀態,進入該狀態後的節點將不會再變化
           static final int CANCELLED =  1;
           //節點狀態值----表示後繼節點在等待當前節點喚醒. 後繼節點入隊時, 會將前繼節點的狀態更新爲此狀態.
           static final int SIGNAL    = -1;
           //節點狀態值----表示線程正在等待狀態  這個狀態只在condition await時設置
           static final int CONDITION = -2;
           //節點狀態值----表示共享模式下, 前繼節點不僅會喚醒其後繼節點, 同事也可能喚醒後繼節點的後繼節點.
           static final int PROPAGATE = -3;
 
           //節點狀態, 節點在獲取鎖和釋放鎖的狀態. 上面4 個節點狀態對應此變量.
           volatile int waitStatus;
           //前驅指針
           volatile Node prev;      
           //後繼指針
           volatile Node next;
           //記錄阻塞的線程
           volatile Thread thread;
           //condition 中是記錄下一個節點, 
           //Lock 中是記錄當前的 node 是獨佔 node 還是共享 node
           Node nextWaiter;
           //如果當前節點是以共享模式等待,則返回 true.
           final boolean isShared() {
               return nextWaiter == SHARED;
           }
           //返回前驅節點
           final Node predecessor() throws NullPointerException {
               Node p = prev;
               if (p == null)
                   throw new NullPointerException();
               else
                   return p;
           }
           //構造函數1 不存放任何線程, 用於生成哨兵節點
           Node() {    // Used to establish initial head or SHARED marker
           }
           //構造函數 2 用於鎖
           Node(Thread thread, Node mode) {     // Used by addWaiter
               this.nextWaiter = mode;
               this.thread = thread;
           }
           //構造函數 3 用於 Condition
           Node(Thread thread, int waitStatus) { // Used by Condition
               this.waitStatus = waitStatus;
               this.thread = thread;
           }
       }

這裏需要說明一下, AQS 的同步隊列是有些特別的, 其 head 節點是一個空節點也可稱爲哨兵節點, 沒有記錄線程 node.thread = null, 其後繼節點纔是實質性的有線程的節點, 這樣做的好處是. 當最後一個有線程的節點出隊後, 不需要想着清空隊列, 同時下次有新節點入隊也不需要重新實例化隊列. 所以隊列爲空時, head = tail = null, 當第一個線程節點入隊時, 會先初始化, head, tail 先指向一個空節點. 再將新節點作爲當前 tail 的下一個節點.通過 CAS 設置成功後, 將新節點設置爲新的 tail 節點即可. 入隊,出隊操作後面分析 ReentrantLock 的時候會分析到. 這裏不再進行說明.

弄明白了原理及節點這些後, 下面我們從 ReentrantLock 來解讀 AQS.


 

五. 從 ReentrantLock 來看 AQS

5.1 ReentrantLock 結構

ReentrantLock 是可重入鎖. 重入鎖的概念在上面 4.3 資源的獲取與釋放中已經解釋過. 忘記的可以翻看一下.
上面說過 AQS 是爲了讓子類通過繼承 AQS 並實現它的抽象方法來管理同步狀態的. AQS 的子類應當被定義爲內部類, 作爲內部的 helper 對象. 那我們先看 ReentrantLock 的結構是否是這樣的.

   public class ReentrantLock implements Lock, java.io.Serializable {
        ...
       abstract static class Sync extends AbstractQueuedSynchronizer {
         abstract void lock();
         ...
       }
       static final class FairSync extends Sync {
         ...
       }

       static final class NonfairSync extends Sync {
          ...
       }
       ...
   }

可以看到 ReentrantLock 內部的 Sync 對象繼承了 AQS, 但是 Sync 又是一個靜態抽象類, FairSyncNonfairSync 又都繼承了 Sync. 這兩個方法類分別是 ReentrantLock 內部實現的 公平鎖與非公平鎖. 他們分別都實現了 Sync 內部的 lock 方法. 那麼這兩個對象又是什麼時候創建的呢. 現在跳轉到 ReentrantLock 的構造函數.

ReentrantLock 的有兩個構造函數, 分別如下.

    public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

我們平時使用的基本都是非公平鎖. 也就是無參的構造函數. 通過調用構造函數及傳入參數的不同, 創建的鎖也不相同. 我們來分析非公平鎖也就是 NonfairSync 的獲取鎖與釋放鎖的流程. 其實他們兩個差不多, 無非就是多了一個判斷條件, 這點在最後會單獨來分析一下.

非公平鎖: 不管是否有等待隊列, 如果可以獲取鎖, 則立刻佔有鎖.

公平鎖: 講究先來先到. 線程在獲取鎖時, 如果這個鎖的等待隊列中已經有線程在等待, 那麼當前線程就會進入等待隊列中.

 

5.2 從 ReentrantLock.lock() 開始

假如現在有 A, B 兩個線程來獲取鎖. 並且我們創建的是非公平鎖, 也就是通過無參構造創建的 ReentrantLock, 那麼調用的 lock 方法, 其實是調用了 NonfairSynclock. 方法.

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            //通過 CAS 改變 AQS 中 state 的值. 期望是 0, 改爲 1, 成功返回 true.
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        ...
    }

線程A 進來執行 if 內語句, 線程 B 進來執行 else 語句.

  • A 線程執行到 lock() 內部的 compareAndSetState(0,1) 的時候, AQS 中 state 值默認是 0, 所以這裏成立, 返回了 true, 然後調用 setExclusiveOwnerThread 方法記錄下獨佔模式下鎖持有者的線程.
  • 當 B 線程執行 if 內的語句的時候, 返回的就是 false 了, 因爲期望值不對. 就調用了 AQS 的 acquire(1).
     
5.3 AbstractQueuedSynchronizer.acquire
      public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  implements java.io.Serializable {
        ...
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
        protected boolean tryAcquire(int arg) {
            throw new UnsupportedOperationException();
        }
        ...
       }

這裏又調用了 tryAcquire, 在學習 AQS 中就說過, 自定義同步器時需要實現的幾個方法.

  • tryAcquire(int arg) : 獨佔式. 嘗試獲取資源. 成功返回 true, 失敗返回 false.
  • tryRelease(int) : 獨佔式. 嘗試釋放資源. 成功返回 true, 失敗返回 false.

所以這裏調用的是 ReentrantLock.NonfairSync 中重寫的 tryAcquire 方法. 假如返回了 false, 那麼取反得 true, 就又會調用 addWaiter(Node.EXCLUSIVE)acquireQueued(), 那麼還是先來看 tryAcquire()

 

5.4 ReentrantLock.NonfairSync.tryAcquire()
    static final class NonfairSync extends Sync {
        ...
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

nonfairTryAcquire 方法在 Sync 中. 直接進入.
 

5.5 ReentrantLock.Sync.nonfairTryAcquire()
       //參數值爲 1.
       final boolean nonfairTryAcquire(int acquires) {
           //獲得當前線程對象
           final Thread current = Thread.currentThread();
           //獲得共享資源狀態, 
           int c = getState();
           //是 0 表示未被佔用. 這裏判斷的目的是有可能 B 線程剛進入, A 線程就執行完了. 那麼 B 就可以直接佔用資源
           if (c == 0) {
               //佔用資源, 改變狀態爲 1.
               if (compareAndSetState(0, acquires)) {
                   //記錄 B 線程.
                   setExclusiveOwnerThread(current);
                   return true;
               }
           }
           //判斷當前要獲取鎖的線程是不是之前記錄的線程. 也就是判斷是不是重入.
           else if (current == getExclusiveOwnerThread()) {
               //如果是重入, 那麼資源的狀態值就 +1.
               int nextc = c + acquires;
               if (nextc < 0) // overflow
                   throw new Error("Maximum lock count exceeded");
               //改變狀態值.
               setState(nextc);
               return true;
           }
           return false;
       }

B 線程嘗試拿鎖失敗, 那麼要怎麼辦呢, 下一步應該就是要加入到等待隊列中了.
B 線程執行到這裏的時候, 假設 A 線程還在執行, 那麼 ifelse if 都不成立, 則直接返回 false. 在 5.3 中這個返回值取反爲 true 後就會調用 acquireQueued, 參數是 addWaiter 方法的返回值和 1. 先進去看一下 addWaiter

      if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();

 

5.6 AbstractQueuedSynchronizer.addWaiter()
    //調用此方法的時候, 傳入的 Node 對象爲 Node.EXCLUSIVE, 獨佔模式.
    //還記得在 AQS 中的內部的 Node 中, 獨佔模式的聲明嗎? static final Node EXCLUSIVE = null;
    //所以這裏傳入的是一個 null.
    private Node addWaiter(Node mode) {
        //將 B 線程封裝成一個 Node 節點. 模式爲獨佔模式.
        Node node = new Node(Thread.currentThread(), mode);
        //將尾指針賦值給 pred.
        Node pred = tail;
        //判斷 pred 是否爲 null
        if (pred != null) {
           //不爲 null, 就將新節點的前驅節點指向隊列的尾部 tail.
            node.prev = pred;
            //通過 CAS 將新節點變爲隊列尾部
            if (compareAndSetTail(pred, node)) {
                //將之前尾部節點的後置節點指向新節點
                pred.next = node;
                //返回新節點
                return node;
            }
        }
       // pred = null 說明還沒有初始化頭尾, 
        enq(node);
        //初始化好頭尾併入隊成功後,返回 B 線程封裝的節點.
        return node;
    }

    //cas 自旋入隊到尾部
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //隊列爲空, 就創建一個空節點(哨兵節點),並將 head 與 tail 指向它
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else { //第二次自旋就進入到 else 中
                //新節點的前驅節點指向第一次自旋時創建的尾部空節點.因爲目前隊列中除了哨兵節點外沒有節點存在
                node.prev = t;
                //將尾指針指向新節點
                if (compareAndSetTail(t, node)) {
                    //將哨兵節點的後置節點指向新節點.
                    t.next = node;
                    return t;
                }
            }
        }
    }

通過前面的 嘗試拿鎖 tryAcquireaddWaiter 表示 B線程拿鎖失敗, 已經被加入到等待隊列的隊尾了, 那麼下一步要幹什麼呢? 那就是再嘗試拿一次鎖, 因爲有可能這個時間,A 線程執行完了. 如果再一次拿鎖失敗, 那麼就需要進入到阻塞狀態了, 直到 A 線程釋放鎖然後喚醒 B 線程. acquireQueued 方法就是完成了這幾步的操作, 那麼現在接着返回到5.3, 看 acquireQueued 方法

     if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
          selfInterrupt();

 

5.7 AbstractQueuedSynchronizer.acquireQueued
      final boolean acquireQueued(final Node node, int arg) {
          //表示是否成功獲取資源
          boolean failed = true;
          try {
              //是否中斷
              boolean interrupted = false;
              //自旋
              for (;;) {
                  //獲取到 B 線程節點的前驅節點. 也就是哨兵節點. 
                  //爲啥不是 A 線程節點? 因爲 A 線程最先搶佔到資源, 都沒有入隊, 直接就執行了. 所以隊列中沒有 A 線程節點.
                  final Node p = node.predecessor();
                  //如果前驅節點是哨兵節點, 即 B 線程節點是第二位. 那麼就有資格去再次嘗試獲取鎖.
                  // 這時候 A 線程如果還沒執行完, 那麼執行 tryAcquire 也就是第5步, 返回的肯定也是 false. 不成立, 所以不進入.
                  // 如果 A 線程在這個時刻執行完了, 那麼執行第5步的時候就會返回 true. 進入 if 內
                  if (p == head && tryAcquire(arg)) {
                      //拿到鎖後, 將 head 指向 B 線程節點, 
                      // 並將 B 線程節點中的 node.thread = null, 
                      //同時斷開B 線程節點的前驅節點也就是哨兵節點的引用 node.prev = null.
                      //所以 head 所指的標杆結點,就是當前獲取到資源的那個結點或null。
                      setHead(node);
                      //setHead 中 已經將線程 B的節點 node.prev 設置爲 null, 這裏再將 head.next 也設置爲 null
                      // 就是爲了方便 GC 回收以前的 head 節點
                      p.next = null; // help GC
                      //成功獲取資源
                      failed = false;
                      //返回等待過程中是否被中斷過.
                      return interrupted;
                  }
                  //如果B 線程執行到這裏的時候, A 線程未釋放資源, 那麼就通過 lockSupport.park 進入阻塞狀態. 直到被 unpark 喚醒
                  // 同時如果不可中斷的情況下被中斷了, 那麼會從 park 中醒來, 發現拿不到鎖, 從而繼續進入到 park阻塞狀態.
                  if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                      //如果等待中被中斷, 改變 interrupted 標誌位.
                      interrupted = true;
              }
          } finally {
              //等待過程中沒有成功獲取資源, 那麼取消節點在隊列中的等待.
              if (failed)
                  cancelAcquire(node);
          }
      }

期間調用了 shouldParkAfterFailedAcquire(哨兵節點, B 線程節點) 通過前驅節點判斷當前節點是否需要進入阻塞 與 parkAndCheckInterrupt() 阻塞線程方法, 先來看着兩個方法具體實現過程.
 

5.8 AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire()
      //當 B 線程執行到這裏時, 傳入參數爲 pred = 哨兵節點, node = B 線程節點
      //如果還有 C 線程, 那麼 pred = B 線程節點, node = C  線程節點.
      private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
          //獲得前驅節點的狀態. 默認爲 0.
          int ws = pred.waitStatus;
          // 如果前驅節點的狀態是 Node.SIGNAL, 說明已經通知前驅節點釋放鎖時通知當前節點, 那麼當前節點就可以進入阻塞狀態了. 返回 true.
          // 在 4.4 Node 節點說明中, 已經寫明 
          //  Node.SIGNAL 表示後繼節點在等待當前節點喚醒. 後繼節點入隊時, 會將前繼節點的狀態更新爲此狀態.
          if (ws == Node.SIGNAL)
              return true;
          // 前驅節點狀態 > 0  說明前驅節點已經放棄獲取鎖了, 
          // 那麼就一直往前找, 直到找到一個正常等待的狀態的節點, 並排在它後面, 
          // 由於那些已經放棄獲取鎖的節點, 由於被加塞到它們前面, 那些節點相當於形成了一個無引用的鏈,  稍後會被GC 回收.
          if (ws > 0) {
              do {
                  node.prev = pred = pred.prev;
              } while (pred.waitStatus > 0);
              pred.next = node;
          } else {
              //因爲當前 B 線程節點的前驅節點是哨兵節點, 狀態默認是 0 ,所以會執行這句代碼.
              //把前驅節點的狀態,也就是哨兵節點的狀態設置爲 -1, 並且返回了 false. 然後在第7步內,再次進行自旋.
              compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
          }
          return false;
      }

由於在鎖的使用場景內, NodewaitStatus 初始值必定是 0 , 因此在這方法首次進入的時候, 前驅節點的狀態必定是 0, 所以會先修改哨兵節點的狀態值爲 -1. 然後會返回 false. 那麼接着會回到 5.7 if 條件內的第一個不成立, 再次進行自旋, 如果期間 A 線程也未執行完成, 那麼會再次調用 shouldParkAfterFailedAcquire 方法. 當第二次進入到這個方法的時候, if (ws == Node.SIGNAL) 這個判斷就會成立, 直接返回 true. (第一次將前驅節點的狀態設置爲 -1, 第二次進入就返回 true ). 那麼按照之前說的, 當前節點已經可以進入阻塞狀態了, 接着執行 5.7 中阻塞線程方法 parkAndCheckInterrupt ().

     if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                //如果等待中被中斷, 改變 interrupted 標誌位.
                interrupted = true;

 

5.9 AbstractQueuedSynchronizer.parkAndCheckInterrupt()
    private final boolean parkAndCheckInterrupt() {
        //走到這裏, 線程 B 纔算是被阻塞, 然後等待被喚醒.
        LockSupport.park(this);
        //被喚醒後, 纔會執行這行代碼. 
       // 被喚醒後, 查看自己是不是被中斷的.
        return Thread.interrupted();
    }

 

5.10 lock 小結

至此, B 線程成功入隊阻塞. 在 5.7 中代碼執行到 parkAndCheckInterrupt() 阻塞後, 就暫停了向下執行. 等待被喚醒了.
那麼現在簡單的總結一下 acquireQueued() 方法內部的流程

  • 調用 lock() 方法, 直接通過 CAS 修改共享資源 state 狀態,
    • 修改成功表示獲取到鎖, 並記錄當前線程.
    • 修改失敗調用 acquire () 獲取鎖.
  • acquire () 獲取鎖方法內會先調用 tryAcquire() --> nonfairTryAcquire(), 在 tryAcquire() 方法返回 false 的情況下才會調用 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    • nonfairTryAcquire() 內先判斷共享資源state是否爲 0 . 爲 0 則通過 CAS 修改共享資源爲 1, 並記錄當前線程. 返回 true.
    • state 不爲 0, 接着判斷記錄的線程是否是當前線程, 這個判斷表示是否重入, 是重入則共享資源 state 值累加 1. 並返回 true.
    • 在共享資源不爲 0, 並且不是重入的情況下, 直接返回 false.
  • acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 方法內, 會先調用 addWaiter() 入隊
    • addWaiter() --> enq(), 在 addWaiter() 方法內先將當前線程封裝爲一個節點.模式爲獨佔模式.
    • 接着判斷隊尾 tail 是否爲 null, 也就是判斷是否初始化過隊列.
      • 如果 tail 不爲 null, 通過 CAS 自旋將新節點加入到隊尾. 並返回新節點.
      • tailnull, 調用 enq() 方法自旋初始化head,tail後將新節點加入到隊尾,
  • 入隊成功後執行執行 acquireQueued(新節點, 1) 阻塞線程. acquireQueued() 方法也是自旋方法.
    • 先獲取新節點的前驅節點,
    • 如果前驅節點是 head 並且再次嘗試拿鎖成功, 執行出隊和換head操作.
    • 前驅節點不是 head 或者嘗試拿鎖失敗, 進入阻塞判斷方法 shouldParkAfterFailedAcquire()
  • shouldParkAfterFailedAcquire() 只會執行 2 次, 最後會返回 true, 表示可以進入阻塞狀態.
  • acquireQueued() 方法內又會調用 parkAndCheckInterrupt() 方法使用 LockSupport.park() 阻塞當前線程. 等待被喚醒.

到這裏, 獲取鎖的流程已經分析完了. 接下來就是釋放鎖與喚醒了.
 

5.11 從 ReentrantLock.unLock() 開始

假如這時候, A 線程已經執行完成, 並且調用了 unLock 那麼代碼如下.

    public void unlock() {
        sync.release(1);
    }

平時我們調用 unLock 後, 調用了 Sync.release(1) 方法, 但是 AQS 中 release 方法與 acquire 方法一樣, 都是 final 類型的, 所以執行的還是 AbstractQueuedSynchronizer.release(1)
 

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

這個方法內部和上面分析的 tryAcquire () 一樣, 都是需要自定義的同步器去實現的. 我們進ReentrantLock.Sync.tryRelease(1)去看一下.
值得注意的是, 在成功釋放鎖之後( tryRelease 返回 true之後), 喚醒後繼節點只是一個 "附加操作", 無論該操作結果怎樣, 最後 release 操作都會返回 true.
 

5.13 ReentrantLock.Sync.tryRelease()
        //傳入的值爲 1.
        protected final boolean tryRelease(int releases) {
            //如果這裏沒有重入, 那麼 getState() 值就爲 1, 1-1 =0 , 表示釋放共享資源
            int c = getState() - releases;
            //釋放鎖的線程當前必須是持有鎖的線程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                //清除記錄的線程
                setExclusiveOwnerThread(null);
            }
            //設置鎖的狀態爲未佔用.
            setState(c);
            return free;
        }

正常來說 tryRelease () 都會成功的. 因爲這是獨佔模式. 它來釋放鎖, 那麼肯定是已經拿到鎖了. 直接減掉相應量的值即可(state -= arg). 不需要考慮線程安全問題. 那麼接着回到 5.12.

      public final boolean release(int arg) {
          //這裏返回了 true.
          if (tryRelease(arg)) {
              //找到 head
              Node h = head;
              // h 如果爲 null, 說明沒有下一個結點了, 因爲如果有下一個實際的節點的話, 在入隊的時候就會初始化了 `head, tail`. 雖然是哨兵節點.
              //在這裏 h != null 成立, 並且, head 的狀態爲-1. 在第8步 shouldParkAfterFailedAcquire 方法設置的.
              if (h != null && h.waitStatus != 0)
                  //喚醒後置線程節點
                  unparkSuccessor(h);
              return true;
          }
          return false;
      }

喚醒後繼的條件是 h != null && h.waitStatus != 0, head 不爲 nullhead 的狀態不是初始狀態, 則喚醒後置. 在獨佔模式下h.waitStatus可能等於0,-1.

 

5.14 AbstractQueuedSynchronizer.unparkSuccessor()
      //參數值爲 head
      private void unparkSuccessor(Node node) {
          // head 狀態爲 -1.
          int ws = node.waitStatus;
          if (ws < 0)
              //通過 CAS 修改 head 狀態爲 0.
              compareAndSetWaitStatus(node, ws, 0);
         // 獲取到 B 線程節點.
          Node s = node.next;
         //喚醒後繼節點的線程, 若爲空, 從 tail 往後遍歷找一個距離`head`最近的正常的節點
         //通常情況下, 要喚醒的節點就是自己的後置節點. 如果後置節點在等待鎖就直接喚醒.
         //但是 上面 5.8 也說過, 也有可能存在後繼節點放棄等待鎖的情況.
          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;
          }
         //喚醒 B 線程.
          if (s != null)
              LockSupport.unpark(s.thread);
      }

這裏有個疑問, 爲什麼要從隊尾逆向向前查找? 而不是直接從 head 開始向後查找呢? 這樣只要正向找到第一個, 是不是就可以停止了. 這裏這樣設計的原因是爲了照顧新入隊的節點, 這裏又會回到上面的 5.6 addWaiter

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        //將尾指針賦值給 pred.
        Node pred = tail;
        //判斷 pred 是否爲 null
        if (pred != null) {
           //將新節點的前驅節點指向隊列的尾部 tail. 
            node.prev = pred;   //-----------------  step1
            //通過 CAS 將新節點變爲隊列尾部
            if (compareAndSetTail(pred, node)) {  //-----------------  step2
                //將之前尾部節點的後置節點指向新節點
                pred.next = node; //  -----------------  step3
                //返回新節點
                return node;
            }
        }
        enq(node);
        return node;
    }

仔細觀察可以發現, 節點入隊並不是一個原子操作, 雖然用了 compareAndSetTail 操作保證了將新節點變爲尾節點, 但是隻能保證step1step2 是執行完成的. 有可能在執行 step3 的時候, 就有別的線程釋放了鎖調用了 unparkSuccessor() 方法.那麼此時 step3 還沒執行, 還未將之前尾節點的後置節點指向新節點. 所以如果從前向後遍歷的話, 是遍歷不到我們新加入的節點的. 還未形成引用關係.
但是因爲在 step2 中已經將尾節點設置成功, 同時在 step1 中也將新節點的前驅節點指向了前尾節點. 所以如果從後往前遍歷的話, 新的尾結點是可以遍歷到, 而且前驅的前尾結點也建立了關係, 可以一直向前查找.

現在 B 線程被喚醒了, 那麼接下來的流程是怎麼樣的呢. 還記得 B 線程是在哪裏被阻塞的嗎? 是在 5.9, 下面是代碼.

   private final boolean parkAndCheckInterrupt() {
       //走到這裏, 線程 B 纔算是被阻塞, 然後等待被喚醒.
       LockSupport.park(this);
       //被喚醒後, 纔會執行這行代碼. 
      // 被喚醒後, 查看自己是不是被中斷的.
       return Thread.interrupted();
   }

被喚醒後, 執行了 Thread.interrupted(). 我們知道這個方法函數返回的是當前正在執行線程的中斷狀態,並清除它. 因爲 B 線程沒有被中斷, 所以這裏返回的是 false. 接下來又回到了 5.7 acquireQueued 方法中調用了 parkAndCheckInterrupt()if 判斷

      final boolean acquireQueued(final Node node, int arg) {
          boolean failed = true;
          try {
              boolean interrupted = false;
              for (;;) {
                  //B 線程被喚醒後, 再次自旋一次, p 還是爲 head 節點.
                  final Node p = node.predecessor();
                  //第一個條件成立, 再次調用 tryAcquire, 嘗試拿鎖. 這時候已經可以拿到鎖了.state = 0.在上面 5.5 直接就返回了 true.
                  if (p == head && tryAcquire(arg)) {
                      //拿到鎖後, 將 head 指向 B 線程節點, 並將 B 線程節點中的 node.thread = null, 
                      //同時斷開B 線程節點的前驅節點也就是哨兵節點的引用 node.prev = null.
                      //所以 head 所指的標杆結點,就是當前獲取到資源的那個結點或null。
                      setHead(node);
                      //setHead 中 已經將線程 B的節點 node.prev 設置爲 null, 這裏再將 head.next 也設置爲 null
                      // 就是爲了方便 GC 回收以前的 head 節點
                      p.next = null; // help GC
                      //成功獲取資源
                      failed = false;
                      //返回等待過程中是否被中斷過. 沒有被打斷過, 所以返回 false.
                      return interrupted;
                  }/
                  // ------------------------------------直接看這裏 -------------------------------------------
                  //B 線程被喚醒後, parkAndCheckInterrupt() 返回的是 false.那麼 if 條件不成立
                  //方法將再次自旋.
                  if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                      //如果等待中被中斷, 改變 interrupted 標誌位.
                      interrupted = true;
              }
          } finally {
              //failed= false, 所以不會取消排隊, 整個方法結束.
              if (failed)
                  cancelAcquire(node);
          }
      }

 

5.15 unlock 小結

release 方法是獨佔模式下線程釋放鎖的頂層入口, 如果徹底釋放了, 也就是 state = 0, 它會喚醒等待隊列裏的其他線程來獲取資源.

  • 調用unlock 方法後內部調用了 AbstractQueuedSynchronizer.release() 方法. 在其方法內部又調用 tryRelease 嘗試釋放鎖.一般都會釋放成功.
  • 釋放成功後準備喚醒後繼節點, 但是有一個喚醒條件, 就是 if (h != null && h.waitStatus != 0) , h 如果爲 null, 說明沒有下一個節點了, 因爲如果有下一個實際的節點的話, 在入隊的時候就會初始化了 head, tail. 雖然是哨兵節點. 所以在這裏 h != null 成立, 並且, head 的狀態爲 -1. 在 5.8 shouldParkAfterFailedAcquire 方法設置的. 所以條件成立, 調用 AbstractQueuedSynchronizer.unparkSuccessor() 方法喚醒
  • 在喚醒的後置節點的時候, 先將 head的狀態設置爲 0, 接着從 head.next 獲取到要喚醒的線程節點. 調用 LockSupport.unpark(s.thread) 將其喚醒.
  • 喚醒後代碼又回到了第7步中內之前阻塞的地方, 條件不成立, 再次自旋. 嘗試拿鎖成功, 設置 head爲要喚醒的節點, 也就是換頭出隊. 再將原 head 節點的 next 置爲 null. 方便 GC 原 head 節點. 最後整個方法結束.

至此, 從 ReentrantLock 來看 AQS 關於獨佔鎖部分已經分析完了, 有興趣的朋友可以按照這個思路, 執行分析一下 AQS 的共享鎖. 在最後補上之前說的 ReentrantLock 公平鎖與非公鎖的區別.


 

六. 公平鎖與非公平鎖的區別

上面說過, 無論有參還是無參的 ReentrantLock 構造函數, 構建出來的對象, 都會調用 lock 方法, 只是根據創建 ReentrantLock 對象時傳入參數來決定調用的是公平鎖的 lock 還是非公平鎖的 lock.
那麼就先看兩者之間 lock 的區別.

 //非公平鎖
 static final class NonfairSync extends Sync {
     ...
     final void lock() {
         if (compareAndSetState(0, 1))
             setExclusiveOwnerThread(Thread.currentThread());
         else
             acquire(1);
     }
    ...
 }
 //公平鎖
 static final class FairSync extends Sync {
   ...
   final void lock() {
       acquire(1);
   }

這裏就能看出一點區別, 非公平鎖在 lock 的時候, 都會先去通過 CAS 改變 state 的值, 看是否能夠成功, 也就是說, 非公平鎖每次都先會嘗試插隊. 不管能不能插隊成功, 先插了再說. 如果插隊失敗, 那就是和公平鎖一樣, 都調用了 AbstractQueuedSynchronizer. acquire() 方法. 在 AbstractQueuedSynchronizer. acquire() 方法內, 又都調用了各自實現的 tryAcquire() 方法. 那麼接着看着兩種鎖各自實現的 tryAcquire().

public class ReentrantLock implements Lock, java.io.Serializable {
    //非公平鎖
    static final class NonfairSync extends Sync {
        ..
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    ...
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }           
    ...
    //公平鎖
    static final class FairSync extends Sync {
       ...
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //區別在這裏
                if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
}

非公平鎖與公平鎖的 tryAcqure() 方法實現, 區別就在 公平鎖的 if 判斷內多了一個條件. !hasQueuedPredecessors()

!hasQueuedPredecessors() 是什麼呢

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

簡單就是說判斷等待隊列中是否存在有效的節點, 通過這個判斷得知公平鎖在lock 的時候會先判斷等待隊列是否有有效的節點存在, 要拿鎖的線程是否需要排隊.

公平鎖與非公平鎖的區別

  • 在調用 lock 方法的時候, 非公平鎖會先拿一次鎖, 拿不到纔去執行自己實現的 tryAcquire() 方法. 而公平鎖則會直接調用自己實現的 tryAcquire().
  • tryAcquire() 方法內, 公平鎖會多一個判斷, 判斷當前等待隊列中是否存在有效的節點, 看是否需要排隊.

這就是他們的區別. 後續流程都基本一致. 本章內容到此就結束了, 如果能看到這裏, 說明你的毅力還真是強大. 若對你有所幫助, 請點贊關注走一波.

以上內容如有分析錯誤, 還請留言, 大家一起探討.

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