由淺入深逐步講解Java併發的半壁江山AQS

Python實戰社羣

Java實戰社羣

長按識別下方二維碼,按需求添加

掃碼關注添加客服

進Python社羣▲

掃碼關注添加客服

進Java社羣

作者丨sowhat1412

來源丨sowhat1412(ID:sowhat9094)

1、JUC的由來

synchronized 關鍵字是JDK官方人員用C++代碼寫的,在JDK6以前是重量級鎖。Java大牛 Doug Lea對 synchronized 在併發編程條件下的性能表現不滿意就自己寫了個JUC,以此來提升併發性能,本文要講的就是JUC併發包下的AbstractQueuedSynchronizer

在JUC中 CountDownLatch、ReentrantLock、ThreadPoolExecutor、ReentrantReadWriteLock 等底層用的都是AQS,AQS幾乎佔據了JUC併發包裏的半壁江山,如果想要獲取鎖可以被中斷、超時獲取鎖、嘗試獲取鎖那就用AQS吧

DougLea傑作: HashMap、JUC、ConcurrentHashMap等。

溫馨提醒

涉及到AQS重要方法、lock、unlock、CountDownLatch、await、signal幾個重要組件的底層講解所以內容有點長

2、AQS前置知識點

2.1、模板方法

AbstractQueuedSynchronizer是個抽象類,所有用到方法的類都要繼承此類的若干方法,對應的設計模式就是模版模式

模版模式定義:一個抽象類公開定義了執行它的方法的方式/模板。它的子類可以按需要重寫方法實現,但調用將以抽象類中定義的方式進行。這種類型的設計模式屬於行爲型模式。

抽象類:

public abstract class SendCustom {
 public abstract void to();
 public abstract void from();
 public void date() {
  System.out.println(new Date());
 }
 public abstract void send();
 // 注意此處 框架方法-模板方法
 public void sendMessage() {
  to();
  from();
  date();
  send();
 }
}

模板方法派生類:

public class SendSms extends SendCustom {

 @Override
 public void to() {
  System.out.println("sowhat");
 }

 @Override
 public void from() {
  System.out.println("xiaomai");
 }

 @Override
 public void send() {
  System.out.println("Send message");
 }
 
 public static void main(String[] args) {
  SendCustom sendC = new SendSms();
  sendC.sendMessage();
 }
}

2.2、LookSupport

LockSupport 是一個線程阻塞工具類,所有的方法都是靜態方法,可以讓線程在任意位置阻塞,當然阻塞之後肯定得有喚醒的方法。常用方法如下:

public static void park(Object blocker); // 暫停當前線程
public static void parkNanos(Object blocker, long nanos); // 暫停當前線程,不過有超時時間的限制
public static void parkUntil(Object blocker, long deadline); // 暫停當前線程,直到某個時間
public static void park(); // 無期限暫停當前線程
public static void parkNanos(long nanos); // 暫停當前線程,不過有超時時間的限制
public static void parkUntil(long deadline); // 暫停當前線程,直到某個時間
public static void unpark(Thread thread); // 恢復當前線程
public static Object getBlocker(Thread t);

park是因爲park英文意思爲停車。我們如果把Thread看成一輛車的話,park就是讓車停下,unpark就是讓車啓動然後跑起來。

與Object類的wait/notify機制相比,park/unpark有兩個優點:

  1. thread爲操作對象更符合阻塞線程的直觀定義

  2. 操作更精準,可以準確地喚醒某一個線程(notify隨機喚醒一個線程,notifyAll 喚醒所有等待的線程),增加了靈活性。

park/unpark調用的是 Unsafe(提供CAS操作) 中的 native代碼。

park/unpark 功能在Linux系統下是用的Posix線程庫pthread中的mutex(互斥量),condition(條件變量)來實現的。mutexcondition保護了一個 _counter 的變量,當 park 時,這個變量被設置爲0。當unpark時,這個變量被設置爲1。

2.3、CAS

CAS 是 CPU指令級別實現了原子性的比較和交換(Conmpare And Swap)操作,注意CAS不是鎖只是CPU提供的一個原子性操作指令。CAS在語言層面不進行任何處理,直接將原則操作實現在硬件級別實現,之所以可以實現硬件級別的操作核心是因爲CAS操作類中有個核心類UnSafe類。

關於CAS引發的ABA問題、性能開銷問題、只能保證一個共享變量之間的原則性操作問題,以前CAS中寫過,在此不再重複講解。

注意:並不是說 CAS 一定比SYN好,如果高併發執行時間久 ,用SYN好, 因爲SYN底層用了wait() 阻塞後是不消耗CPU資源的。如果鎖競爭不激烈說明自旋不嚴重,此時用CAS。

3、AQS重要方法

模版方法分爲獨佔式共享式,子類根據需要不同調用不同的模版方法(講解有點多,想看底層可直接下滑到第四章節)。

3.1 模板方法

3.1.1 獨佔式獲取
3.1.1.1 accquire

不可中斷獲取鎖accquire是獲取獨佔鎖方法,acquire嘗試獲取資源,成功則直接返回,不成功則進入等待隊列,這個過程不會被線程中斷,被外部中斷也不響應,獲取資源後纔再進行自我中斷selfInterrupt()

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}
  1. acquire(arg) tryAcquire(arg) 顧名思義,它就是嘗試獲取鎖,需要我們自己實現具體細節,一般要求是:

如果該鎖沒有被另一個線程保持,則獲取該鎖並立即返回,將鎖的保持計數設置爲 1。

如果當前線程已經保持該鎖,則將保持計數加 1,並且該方法立即返回。

如果該鎖被另一個線程保持,則出於線程調度的目的,禁用當前線程,並且在獲得鎖之前,該線程將一直處於休眠狀態,此時鎖保持計數被設置爲 1。

  1. addWaiter(Node.EXCLUSIVE)

主要功能是 一旦嘗試獲取鎖未成功,就要使用該方法將其加入同步隊列尾部,由於可能有多個線程併發加入隊尾產生競爭,因此採用compareAndSetTail鎖方法來保證同步

  1. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

一旦加入同步隊列,就需要使用該方法,自旋阻塞 喚醒來不斷的嘗試獲取鎖,直到被中斷或獲取到鎖。

3.1.1.2 acquireInterruptibly

可中斷獲取鎖acquireInterruptibly相比於acquire支持響應中斷。

1、如果當前線程未被中斷,則嘗試獲取鎖。

2、如果鎖空閒則獲鎖並立即返回,state = 1。

3、如果當前線程已持此鎖,state + 1,並且該方法立即返回。

4、如果鎖被另一個線程保持,出於線程調度目的,禁用當前線程,線程休眠ing,除非鎖由當前線程獲得或者當前線程被中斷了,中斷後會拋出InterruptedException,並且清除當前線程的已中斷狀態。

5、此方法是一個顯式中斷點,所以要優先考慮響應中斷。

 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
     throw new InterruptedException(); // acquireInterruptibly 選擇
      interrupted = true; // acquire 的選擇
3.1.1.3 tryAcquireNanos

該方法可以被中斷,增加了超時則失敗的功能。可以說該方法的實現與上述兩方法沒有任何區別。時間功能上就是用的標準超時功能,如果剩餘時間小於0那麼acquire失敗,如果該時間大於一次自旋鎖時間(spinForTimeoutThreshold = 1000),並且可以被阻塞,那麼調用LockSupport.parkNanos方法阻塞線程。

doAcquireNanos內部:

  nanosTimeout = deadline - System.nanoTime();
  if (nanosTimeout <= 0L)
      return false;
  if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
      LockSupport.parkNanos(this, nanosTimeout);
  if (Thread.interrupted())
      throw new InterruptedException();

該方法一般會有以下幾種情況產生:

  1. 在指定時間內,線程獲取到鎖,返回true。

  2. 當前線程在超時時間內被中斷,拋中斷異常後,線程退出。

  3. 到截止時間後線程仍未獲取到鎖,此時線程獲得鎖失敗,不再等待直接返回false。

3.1.2 共享式獲取
3.1.2.1 acquireShared
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

該模版方法的工作:

  1. 調用tryAcquireShared(arg) 嘗試獲得資源,返回值代表如下含義:

負數表示失敗。

0 表示成功,但沒有剩餘可用資源。

正數表示成功,且有剩餘資源。

doAcquireShared作用:

創建節點然後加入到隊列中去,這一塊和獨佔模式下的 addWaiter 代碼差不多,不同的是結點的模式是SHARED,在獨佔模式 EXCLUSIVE。

3.1.2.2 acquireSharedInterruptibly

無非就是可中斷性的共享方法

public final void acquireSharedInterruptibly(long arg)  throws InterruptedException {
    if (Thread.interrupted()) // 如果線程被中斷,則拋出異常
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)  
        // 如果tryAcquireShared()方法獲取失敗,則調用如下的方法
        doAcquireSharedInterruptibly(arg);
}
3.1.2.3. tryAcquireSharedNanos

嘗試以共享模式獲取,如果被中斷則中止,如果超過給定超時期則失敗。實現此方法首先要檢查中斷狀態,然後至少調用一次 tryacquireshared(long),並在成功時返回。否則,在成功、線程中斷或超過超時期之前,線程將加入隊列,可能反覆處於阻塞或未阻塞狀態,並一直調用 tryacquireshared(long)

    public final boolean tryAcquireSharedNanos(long arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquireShared(arg) >= 0 ||
            doAcquireSharedNanos(arg, nanosTimeout);
    }
3.1.3 獨佔式釋放

獨佔鎖的釋放調用unlock方法,而該方法實際調用了AQS的release方法,這段代碼邏輯比較簡單,如果同步狀態釋放成功(tryRelease返回true)則會執行if塊中的代碼,當head指向的頭結點不爲null,並且該節點的狀態值不爲0的話纔會執行unparkSuccessor()方法。

    public final boolean release(long arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
3.1.4 共享式釋放

releaseShared首先去嘗試釋放資源tryReleaseShared(arg),如果釋放成功了,就代表有資源空閒出來,那麼就用doReleaseShared()去喚醒後續結點。

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

比如CountDownLatch的countDown()具體實現:

    public void countDown() {
        sync.releaseShared(1);
    }

3.2 子類需實現方法

子類要實現父類方法也分爲獨佔式共享式

3.2.1 獨佔式獲取

tryAcquire 顧名思義,就是嘗試獲取鎖,AQS在這裏沒有對其進行功能的實現,只有一個拋出異常的語句,我們需要自己對其進行實現,可以對其重寫實現公平鎖、不公平鎖、可重入鎖、不可重入鎖

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}
3.2.2 獨佔式釋放

tryRelease 嘗試釋放 獨佔鎖,需要子類實現。

   protected boolean tryRelease(long arg) {
        throw new UnsupportedOperationException();
    }
3.2.3 共享式獲取

tryAcquireShared 嘗試進行共享鎖的獲得,需要子類實現。

protected long tryAcquireShared(long arg) {
        throw new UnsupportedOperationException();
    }
3.2.4 共享式釋放

tryReleaseShared嘗試進行共享鎖的釋放,需要子類實現。

    protected boolean tryReleaseShared(long arg) {
        throw new UnsupportedOperationException();
    }

3.3  狀態標誌位

state因爲用 volatile修飾 保證了我們操作的可見性,所以任何線程通過getState()獲得狀態都是可以得到最新值,但是setState()無法保證原子性,因此AQS給我們提供了compareAndSetState方法利用底層UnSafe的CAS功能來實現原子性。

    private volatile long state;

    protected final long getState() {
        return state;
    }

    protected final void setState(long newState) {
        state = newState;
    }

   protected final boolean compareAndSetState(long expect, long update) {
        return unsafe.compareAndSwapLong(this, stateOffset, expect, update);
    }

3.4 查詢是否獨佔模式

isHeldExclusively 該函數的功能是查詢當前的工作模式是否是獨佔模式。需要子類實現。

    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

3.5 自定義實現鎖

這裏需要重點說明一點,JUC中一般是用一個子類繼承自Lock,然後在子類中定義一個內部類來實現AQS的繼承跟使用

public class SowhatLock implements Lock
{
 private Sync sync = new Sync();

 @Override
 public void lock()
 {
  sync.acquire(1);
 }

 @Override
 public boolean tryLock()
 {
  return false;
 }

 @Override
 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
 {
  return sync.tryAcquireNanos(1,unit.toNanos(time));
 }

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

 @Override
 public Condition newCondition()
 {
  return sync.newCondition();
 }

 @Override
 public void lockInterruptibly() throws InterruptedException
 {
 }

 private class Sync extends AbstractQueuedSynchronizer
 {
  @Override
  protected boolean tryAcquire(int arg)
  {
   assert arg == 1;
   if (compareAndSetState(0, 1))
   {
    setExclusiveOwnerThread(Thread.currentThread());
    return true;
   }
   return false;
  }

  @Override
  protected boolean tryRelease(int arg)
  {
   assert arg == 1;
   if (!isHeldExclusively())
   {
    throw new IllegalMonitorStateException();
   }
   setExclusiveOwnerThread(null);
   setState(0);
   return true;
  }

  @Override
  protected boolean isHeldExclusively()
  {
   return getExclusiveOwnerThread() == Thread.currentThread();
  }

  Condition newCondition() {
   return new ConditionObject();
  }
 }
}

自定義實現類:

public class SoWhatTest
{
 public static int m = 0;
 public  static CountDownLatch latch  = new CountDownLatch(50);
 public static Lock lock = new SowhatLock();

 public static void main(String[] args) throws  Exception
 {
  Thread[] threads = new Thread[50];
  for (int i = 0; i < threads.length ; i++)
  {
   threads[i] = new Thread(()->{
    try{
     lock.lock();
     for (int j = 0; j <100 ; j++)
     {
      m++;
     }
    }finally
    {
     lock.unlock();
    }
    latch.countDown();
  });
  }
  for(Thread t : threads) t.start();
  latch.await();
  System.out.println(m);
 }
}

4、AQS底層

4.1 CLH

CLH(Craig、 Landin、 Hagersten locks三個人名字綜合而命名):

  1. 是一個自旋鎖,能確保無飢餓性,提供先來先服務的公平性。

  2. CLH 鎖也是一種基於鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,它不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋。

4.2 Node

CLH 隊列由Node對象組成,其中Node是AQS中的內部類。

static final class Node {
 // 標識共享鎖
 static final Node SHARED = new Node();
 // 標識獨佔鎖
 static final Node EXCLUSIVE = null;
 // 前驅節點
 volatile Node prev;
 // 後繼節點
 volatile Node next;
 // 獲取鎖失敗的線程保存在Node節點中。
 volatile Thread thread;
 // 當我們調用了Condition後他也有一個等待隊列
 Node nextWaiter;
 //在Node節點中一般通過waitStatus獲得下面節點不同的狀態,狀態對應下方。
 volatile int waitStatus;
 static final int CANCELLED =  1;
 static final int SIGNAL    = -1;
 static final int CONDITION = -2;
 static final int PROPAGATE = -3;

waitStatus 有如下5中狀態:

  1. CANCELLED = 1

表示當前結點已取消調度。當超時或被中斷(響應中斷的情況下),會觸發變更爲此狀態,進入該狀態後的結點將不會再變化。

  1. SIGNAL = -1

表示後繼結點在等待當前結點喚醒。後繼結點入隊時,會將前繼結點的狀態更新爲 SIGNAL。

  1. CONDITION = -2

表示結點等待在 Condition 上,當其他線程調用了 Condition 的 signal() 方法後,CONDITION狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。

  1. PROPAGATE = -3

共享模式下,前繼結點不僅會喚醒其後繼結點,同時也可能會喚醒後繼的後繼結點。

  1. INITIAL = 0

新結點入隊時的默認狀態。

4.3 AQS實現

4.3.1 公平鎖和非公平鎖

銀行售票窗口營業中:

公平排隊:每個客戶來了自動在最後面排隊,輪到自己辦理業務的時候拿出身份證等證件取票。

非公平排隊:有個旅客火車馬上開車了,他拿着自己的各種證件着急這想跟窗口工作人員說是否可以加急辦理下,可以的話則直接辦理,不可以的話則去隊尾排隊去。

在JUC中同樣存在公平鎖非公平鎖一般非公平鎖效率好一些。因爲非公平鎖狀態下打算搶鎖的線程不用排隊掛起了

4.3.2 AQS細節

AQS內部維護着一個FIFO的隊列,即CLH隊列,提供先來先服務的公平性。AQS的同步機制就是依靠CLH隊列實現的。CLH隊列是FIFO的雙端雙向鏈表隊列(方便尾部節點插入)。線程通過AQS獲取鎖失敗,就會將線程封裝成一個Node節點,通過CAS原子操作插入隊列尾。當有線程釋放鎖時,會嘗試讓隊頭的next節點佔用鎖,個人理解AQS具有如下幾個特點:

  1. 在AQS 同步隊列中 -1 表示線程在睡眠狀態

  2. 當前Node節點線程會把前一個Node.ws = -1。當前節點把前面節點ws設置爲-1,你可以理解爲:你自己能知道自己睡着了嗎?只能是別人看到了發現你睡眠了

  3. 持有鎖的線程永遠不在隊列中

  4. 在AQS隊列中第二個纔是最先排隊的線程

  5. 如果是交替型任務或者單線程任務,即使用了Lock也不會涉及到AQS 隊列

  6. 不到萬不得已不要輕易park線程,很耗時的!所以排隊的頭線程會自旋的嘗試幾個獲取鎖

4.4 加鎖跟解鎖流程圖

以最經典的 ReentrantLock 爲例逐步分析下 lockunlock 底層流程圖(要原圖的話公衆號回覆:lock)。

private Lock lock = new ReentrantLock();
public void test(){
    lock.lock();
    try{
        doSomeThing();
    }catch (Exception e){
      ...
    }finally {
        lock.unlock();
    }
}

4.4.1 獨佔式加入同步隊列

同步器AQS中包含兩個節點類型的引用:一個指向頭結點的引用(head),一個指向尾節點的引用(tail),如果加入的節點是OK的則會直接運行該節點,當若干個線程搶鎖失敗了那麼就會搶着加入到同步隊列的尾部,因爲是搶着加入這個時候用CAS來設置尾部節點。入口代碼:

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

該方法是需要自我實現的,在上面的demo中可見一斑,就是返回是否獲得了鎖。

 protected final boolean tryAcquire(int acquires) {
     final Thread current = Thread.currentThread();
     int c = getState();
     if (c == 0) {
         //  是否需要加入隊列,不需要的話則嘗試CAS獲得鎖,獲得成功後 設置當前鎖的擁有者
         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;
 }
  1. addWaiter(Node.EXCLUSIVE,arg)

/**
 * 如果嘗試獲取同步狀態失敗的話,則構造同步節點(獨佔式的Node.EXCLUSIVE),通過addWaiter(Node node,int args)方法將該節點加入到同步隊列的隊尾。
 */
 private Node addWaiter(Node mode) {
     // 用當前線程構造一個Node對象,mode是一個表示Node類型的字段,或者說是這個節點是獨佔的還是共享的
     Node node = new Node(Thread.currentThread(), mode);
     // 將目前隊列中尾部節點給pred
     Node pred = tail;
     // 隊列不爲空的時候
     if (pred != null) {
         node.prev = pred;
         // 先嚐試通過AQS方式修改尾節點爲最新的節點,如果修改失敗,意味着有併發,
         if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
     //第一次嘗試添加尾部失敗說明有併發,此時進入自旋
     enq(node);
     return node;
 }
  1. 自旋enq 方法將併發添加節點的請求通過CAS跟自旋將尾節點的添加變得串行化起來。說白了就是讓節點放到正確的隊尾位置。

/**
* 這裏進行了循環,如果此時存在了tail就執行同上一步驟的添加隊尾操作,如果依然不存在,
* 就把當前線程作爲head結點。插入節點後,調用acquireQueued()進行阻塞
*/
private Node enq(final Node node) {
   for (;;) {
       Node t = tail;
       if (t == null) { // Must initialize
           if (compareAndSetHead(new Node()))
               tail = head;
       } else {
           node.prev = t;
           if (compareAndSetTail(t, node)) {
               t.next = node;
               return t;
           }
       }
   }
}
  1. acquireQueued 是當前Node節點線程在死循環中獲取同步狀態,而只有前驅節點是頭節點才能嘗試獲取鎖,原因是:

  1. 頭結點是成功獲取同步狀態(鎖)的節點,而頭節點的線程釋放了同步狀態以後,將會喚醒其後繼節點,後繼節點的線程被喚醒後要檢查自己的前驅節點是否爲頭結點。

  2. 維護同步隊列的FIFO原則,節點進入同步隊列之後,會嘗試自旋幾次。

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)) {
            // 節點中的線程循環的檢查,自己的前驅節點是否爲頭節點
            // 只有當前節點 前驅節點是頭節點纔會 再次調用我們實現的方法tryAcquire
            // 接下來無非就是將當前節點設置爲頭結點,移除之前的頭節點
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 否則檢查前一個節點的狀態,看當前獲取鎖失敗的線程是否要掛起
            if (shouldParkAfterFailedAcquire(p, node) &&
           //如果需要掛起,藉助JUC包下面的LockSupport類的靜態方法park掛起當前線程,直到被喚醒
                parkAndCheckInterrupt())
                interrupted = true; // 兩個判斷都是true說明 則置true
        }
    } finally {
        //如果等待過程中沒有成功獲取資源(如timeout,或者可中斷的情況下被中斷了),那麼取消結點在隊列中的等待。
        if (failed)
           //取消請求,將當前節點從隊列中移除
            cancelAcquire(node);
    }
}

如果成功就返回,否則就執行shouldParkAfterFailedAcquireparkAndCheckInterrupt來達到阻塞效果。

  1. shouldParkAfterFailedAcquire 第二步的addWaiter()構造的新節點,waitStatus的默認值是0。此時,會進入最後一個if判斷,CAS設置pred.waitStatus SIGNAL,最後返回false。由於返回false,第四步的acquireQueued會繼續進行循環。假設node的前繼節點pred仍然不是頭結點或鎖獲取失敗,則會再次進入shouldParkAfterFailedAcquire()。上一輪循環中已經將pred.waitStatu = -1了,則這次會進入第一個判斷條件,直接返回true,表示應該阻塞調用parkAndCheckInterrupt

那麼什麼時候會遇到ws > 0呢?當pred所維護的獲取請求被取消時(也就是node.waitStatus = CANCELLED,這時就會循環移除所有被取消的前繼節點pred,直到找到未被取消的pred。移除所有被取消的前繼節點後,直接返回false。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
       int ws = pred.waitStatus; // 獲得前驅節點的狀態
       if (ws == Node.SIGNAL) //此處是第二次設置
           return true;
       if (ws > 0) {
          do {
               node.prev = pred = pred.prev;
           } while (pred.waitStatus > 0);
           pred.next = node;
       } else {
          //  此處是第一次設置 unsafe級別調用設置
          compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
       }
       return false;
   }
  1. parkAndCheckInterrupt 主要任務是暫停當前線程然後查看是否已經暫停了。

private final boolean parkAndCheckInterrupt() {
    // 調用park()使線程進入掛起狀態,什麼時候調用了unpark再繼續執行下面
    LockSupport.park(this); 
    // 如果被喚醒,查看自己是不是已經被中斷了。
    return Thread.interrupted();
}
  1. cancelAcquireacquireQueued方法的finally會判斷 failed值,正常運行時候自旋出來的時候會是false,如果中斷或者timeout了 則會是true,執行cancelAcquire,其中核心代碼是node.waitStatus = Node.CANCELLED

  2. selfInterrupt

static void selfInterrupt() {
      Thread.currentThread().interrupt();
  }
4.4.2 獨佔式釋放隊列頭節點

release()會調用tryRelease方法嘗試釋放當前線程持有的鎖,成功的話喚醒後繼線程,並返回true,否則直接返回false。

public final boolean release(long arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  1. tryRelease 這個是子類需要自我實現的,沒啥說的根據業務需要實現。

  2. unparkSuccessor 喚醒頭結點的後繼節點。

private void unparkSuccessor(Node node) {
   int ws = node.waitStatus; // 獲得頭節點狀態
    if (ws < 0) //如果頭節點裝小於0 則將其置爲0
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next; //這個是新的頭節點
    if (s == null || s.waitStatus > 0) { 
    // 如果新頭節點不滿足要求
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
        //從隊列尾部開始往前去找最前面的一個waitStatus小於0的節點
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)//喚醒後繼節點對應的線程
        LockSupport.unpark(s.thread);
}
4.4.3 AQS 中增加跟刪除形象圖


5、CountDownLatch底層

5.1 共享鎖 CountDownLatch底層

CountDownLatch 雖然相對簡單,但也實現了共享鎖模型。但是如何正確的吹逼 CountDownLatch  呢?如果在理解了上述流程的基礎上,從CountDownLatch入手來看 AQS 中關於共享鎖的代碼還比較好看懂,在看的時候可以 以看懂大致內容爲主,學習其設計的思路,不要陷入所有條件處理細節中,多線程環境中,對與錯有時候不是那麼容易看出來的。個人追源碼繪製瞭如下圖:


5.2 計數信號量Semaphore

Semaphore 這就是共享鎖的一個實現類,在初始化的時候就規定了共享鎖池的大小N,有一個線程獲得了鎖,可用數就減少1個。有一個線程釋放鎖可用數就增加1個。如果有 >=2 的線程同時釋放鎖,則此時有多個鎖可用。這個時候就可以 同時喚醒 兩個鎖 setHeadAndPropagate (流程圖懶的繪製了)。

 public final void acquireShared(int arg) {
     if (tryAcquireShared(arg) < 0)
         doAcquireShared(arg);
 }
private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            //找先驅結點
            final Node p = node.predecessor();
            if (p == head) {
                 // 嘗試獲取資源
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 設置當前結點爲頭結點,然後去喚醒後續結點。注意傳播性 喚醒!
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC  釋放頭結點,等待GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;//獲取到資源
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)//如果最後沒有獲取到資源,則cancel
            cancelAcquire(node);
    }
}

5.3 ReentrantReadWriteLock

ReentrantReadWriteLock 類中也是隻有一個32位的int state來表示讀鎖跟寫鎖,如何實現的?

  1. 後16位用來保存獨享的寫鎖個數,第一次獲得就是01,第二次重入就是10了,這樣的方式來保存。

  2. 但是多個線程都可以獲得讀鎖,並且每個線程可能讀多次,如何保存?我們用前16位來保存有多少個線程獲得了讀鎖

  3. 每個讀鎖線程獲得的重入讀鎖個數 由內部類HoldCounter與讀鎖配套使用。

6、Condition

synchronized 可用 wait()notify()/notifyAll() 方法相結合可以實現等待/通知模式。Lock 也提供了 Condition 來提供類似的功能。

Condition是JDK5後引入的Interface,它用來替代傳統的Object的wait()/notify()實現線程間的協作,相比使用Object的wait()/notify(),使用Conditionawait()/signal()這種方式 實現線程間協作更加安全和高效。簡單說,他的作用是使得某些線程一起等待某個條件(Condition),只有當該條件具備(signal 或者 signalAll方法被調用)時,這些等待線程纔會被喚醒,從而重新爭奪鎖。wait()/notify()這些都更傾向於底層的實現開發,而Condition接口更傾向於代碼實現的等待通知效果。兩者之間的區別與共通點如下:

6.1 條件等待隊列

條件等待隊列,指的是 Condition 內部自己維護的一個隊列,不同於 AQS 的 同步等待隊列。它具有以下特點:

要加入條件等待隊列的節點,不能在 同步等待隊列。

從 條件等待隊列 移除的節點,會進入同步等待隊列。

一個鎖對象只能有一個同步等待隊列,但可以有多個條件等待隊列。

這裏以 AbstractQueuedSynchronizer 的內部類 ConditionObject 爲例(Condition 的實現類)來分析下它的具體實現過程。首先來看該類內部定義的幾個成員變量:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

它採用了 AQSNode 節點構造(前面說過Node類有nextWaiter屬性),並定義了兩個成員變量:firstWaiterlastWaiter 。說明在 ConditionObject 內部也維護着一個自己的單向等待隊列。目前可知它的結構如下:

6.2 await、signal

比如有線程 1、2競爭鎖,下面來說下具體過程 線程1:

1、線程1 調用 reentrantLock.lock時,持有鎖。

2、線程1 調用 await 方法,進入條件等待隊列 ,同時釋放鎖。

3、線程1 獲取到線程2 signal 信號,從條件等待隊列進入同步等待隊列。

線程2:

1、線程2 調用 reentrantLock.lock時,由於鎖被線程1 持有,進入同步等待隊列

2、由於線程1 釋放鎖,線程2 從同步等待隊列 移除,獲取到鎖。

3、線程2 調用 signal 方法,導致線程 1 被喚醒。線程2 調用unlock ,線程1 獲取鎖後繼續下走。

6.2.1 await

當我們看await、signal 的源碼時候不要認爲等待隊列跟同步隊列是完全分開的,其實個人感覺底層源碼是有點 HashMap 中的紅黑樹跟雙向鏈表的意思。

當調用await方法時候,說明當前任務隊列的頭節點拿着鎖呢,此時要把該Thread從任務隊列挪到等待隊列再喚醒任務隊列最前面排隊的運行任務,如圖:

  1. thread 表示節點存放的線程。

  2. waitStatus 表示節點等待狀態。條件等待隊列中的節點等待狀態都是 CONDITION,否則會被清除。

  3. nextWaiter 表示後指針。

6.2.2 signal

當我們調用signal方法的時候,我們要將等待隊列中的頭節點移出來,讓其去搶鎖,如果是公平模式就要去排隊了,流程如圖:

上面只是形象流程圖,如果從代碼級別看的話大致流程如下:

6.2.3 signalAll

signalAll 與 signal 方法的區別體現在 doSignalAll 方法上,前面我們已經知道doSignal方法只會對等待隊列的頭節點進行操作,doSignalAll方法只不過將等待隊列中的每一個節點都移入到同步隊列中,即通知當前調用condition.await()方法的每一個線程:

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null); // 循環
}

6.3 End

一個 Condition 對象就有一個單項的等待任務隊列。在一個多線程任務中我們可以new出多個等待任務隊列。比如我們new出來兩個等待隊列。

 private Lock lock = new ReentrantLock();
 private Condition FirstCond = lock.newCondition();
 private Condition SecondCond = lock.newCondition();

所以真正的AQS任務中一般是一個任務隊列N個等待隊列的,因此我們儘量調用signal而少用signalAll,因爲在指定的實例化等待隊列中只有一個可以拿到鎖的。

Synchronized 中的 waitnotify 底層代碼的等待隊列只有一個,多個線程調用wait的時候我們是無法知道頭節點是那個具體線程的。因此只能notifyAll

7、參考

1、詳解Condition的await和signal:https://www.jianshu.com/p/28387056eeb4

2、Condition的await和signal流程:https://www.cnblogs.com/insaneXs/p/12219097.html

程序員專欄 掃碼關注填加客服 長按識別下方二維碼進羣

近期精彩內容推薦:  

 又一個程序員,被抓捕!(真實事件)

 程序員有個可愛女朋友是種什麼體驗?

 “12306”的架構到底有多牛逼?

 csv文件讀寫亂碼問題的一個簡單解決方法


在看點這裏好文分享給更多人↓↓

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