併發-AQS源碼分析

一、概述

  談到併發,不得不談ReentrantLock;而談到ReentrantLock,不得不談AbstractQueuedSynchronizer(AQS)!

  類如其名,抽象的隊列式的同步器,AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch...。

 

二、框架

  它維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。這裏volatile是核心關鍵詞,具體volatile的語義,在此不述。state的訪問方式有三種:

  • getState()
  • setState()
  • compareAndSetState()

  AQS定義兩種資源共享方式:Exclusive(獨佔,只有一個線程能執行,如ReentrantLock)和Share(共享,多個線程可同時執行,如Semaphore/CountDownLatch)。

  不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:

  • isHeldExclusively():該線程是否正在獨佔資源。只有用到condition才需要去實現它。
  • tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
  • tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
  • tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
  • tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回false。

  以ReentrantLock爲例,state初始化爲0,表示未鎖定狀態。A線程lock()時,會調用tryAcquire()獨佔該鎖並將state+1。此後,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)爲止,其它線程纔有機會獲取該鎖。當然,釋放鎖之前,A線程自己是可以重複獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多麼次,這樣才能保證state是能回到零態的。

  再以CountDownLatch以例,任務分爲N個子線程去執行,state也初始化爲N(注意N要與線程個數一致)。這N個子線程是並行執行的,每個子線程執行完後countDown()一次,state會CAS減1。等到所有子線程都執行完後(即state=0),會unpark()主調用線程,然後主調用線程就會從await()函數返回,繼續後餘動作。

  一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可。但AQS也支持自定義同步器同時實現獨佔和共享兩種方式,如ReentrantReadWriteLock。

三、源碼詳解

  本節開始講解AQS的源碼實現。依照acquire-release、acquireShared-releaseShared的次序來。

3.1 acquire(int)

  此方法是獨佔模式下線程獲取共享資源的頂層入口。如果獲取到資源,線程直接返回,否則進入等待隊列,直到獲取到資源爲止,且整個過程忽略中斷的影響。這也正是lock()的語義,當然不僅僅只限於lock()。獲取到資源後,線程就可以去執行其臨界區代碼了。下面是acquire()的源碼:

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

函數流程如下:

  1. tryAcquire()嘗試直接去獲取資源,如果成功則直接返回;
  2. addWaiter()將該線程加入等待隊列的尾部,並標記爲獨佔模式;
  3. acquireQueued()使線程在等待隊列中獲取資源,一直獲取到資源後才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false。
  4. 如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源後纔再進行自我中斷selfInterrupt(),將中斷補上。

  這時單憑這4個抽象的函數來看流程還有點朦朧,不要緊,看完接下來的分析後,你就會明白了。就像《大話西遊》裏唐僧說的:等你明白了捨生取義的道理,你自然會回來和我唱這首歌的。

3.1.1 tryAcquire(int)

  此方法嘗試去獲取獨佔資源。如果獲取成功,則直接返回true,否則直接返回false。這也正是tryLock()的語義,還是那句話,當然不僅僅只限於tryLock()。如下是tryAcquire()的源碼:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

  什麼?直接throw異常?說好的功能呢?好吧,還記得概述裏講的AQS只是一個框架,具體資源的獲取/釋放方式交由自定義同步器去實現嗎?就是這裏了!!!AQS這裏只定義了一個接口,具體資源的獲取交由自定義同步器去實現了(通過state的get/set/CAS)!!!至於能不能重入,能不能加塞,那就看具體的自定義同步器怎麼去設計了!!!當然,自定義同步器在進行資源訪問時要考慮線程安全的影響。

  這裏之所以沒有定義成abstract,是因爲獨佔模式下只用實現tryAcquire-tryRelease,而共享模式下只用實現tryAcquireShared-tryReleaseShared。如果都定義成abstract,那麼每個模式也要去實現另一模式下的接口。說到底,Doug Lea還是站在咱們開發者的角度,儘量減少不必要的工作量。

3.1.2 addWaiter(Node)

  此方法用於將當前線程加入到等待隊列的隊尾,並返回當前線程所在的結點。還是上源碼吧:

 private Node addWaiter(Node mode) {
      //以給定模式構造結點。mode有兩種:EXCLUSIVE(獨佔)和SHARED(共享)
      Node node = new Node(Thread.currentThread(), mode);
      
      //嘗試快速方式直接放到隊尾。
      Node pred = tail;
      if (pred != null) {
          node.prev = pred;
          if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
     
     //上一步失敗則通過enq入隊。
     enq(node);
     return node;
 }

 不用再說了,直接看註釋吧。

3.1.2.1 enq(Node)

   此方法用於將node加入隊尾。源碼如下:

private Node enq(final Node node) {
      //CAS"自旋",直到成功加入隊尾
      for (;;) {
          Node t = tail;
          if (t == null) { // 隊列爲空,創建一個空的標誌結點作爲head結點,並將tail也指向它。
              if (compareAndSetHead(new Node()))
                  tail = head;
          } else {//正常流程,放入隊尾
              node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
 }

 

如果你看過AtomicInteger.getAndIncrement()函數源碼,那麼相信你一眼便看出這段代碼的精華。CAS自旋volatile變量,是一種很經典的用法。還不太瞭解的,自己去百度一下吧。

3.1.3 acquireQueued(Node, int)

  OK,通過tryAcquire()和addWaiter(),該線程獲取資源失敗,已經被放入等待隊列尾部了。聰明的你立刻應該能想到該線程下一部該幹什麼了吧:進入等待狀態休息,直到其他線程徹底釋放資源後喚醒自己,自己再拿到資源,然後就可以去幹自己想幹的事了。沒錯,就是這樣!是不是跟醫院排隊拿號有點相似~~acquireQueued()就是幹這件事:在等待隊列中排隊拿號(中間沒其它事幹可以休息),直到拿到號後再返回。這個函數非常關鍵,還是上源碼吧:

final boolean acquireQueued(final Node node, int arg) {
      boolean failed = true;//標記是否成功拿到資源
      try {
          boolean interrupted = false;//標記等待過程中是否被中斷過
          
          //又是一個“自旋”!
          for (;;) {
              final Node p = node.predecessor();//拿到前驅
              //如果前驅是head,即該結點已成老二,那麼便有資格去嘗試獲取資源(可能是老大釋放完資源喚醒自己的,當然也可能被interrupt了)。
             if (p == head && tryAcquire(arg)) {
                 setHead(node);//拿到資源後,將head指向該結點。所以head所指的標杆結點,就是當前獲取到資源的那個結點或null。
                 p.next = null; // setHead中node.prev已置爲null,此處再將head.next置爲null,就是爲了方便GC回收以前的head結點。也就意味着之前拿完資源的結點出隊了!
                 failed = false;
                 return interrupted;//返回等待過程中是否被中斷過
             }
             
             //如果自己可以休息了,就進入waiting狀態,直到被unpark()
             if (shouldParkAfterFailedAcquire(p, node) &&
                 parkAndCheckInterrupt())
                 interrupted = true;//如果等待過程中被中斷過,哪怕只有那麼一次,就將interrupted標記爲true
         }
     } finally {
         if (failed)
             cancelAcquire(node);
     }
 }

 

到這裏了,我們先不急着總結acquireQueued()的函數流程,先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具體幹些什麼。

3.1.3.1 shouldParkAfterFailedAcquire(Node, Node)

  此方法主要用於檢查狀態,看看自己是否真的可以去休息了,萬一隊列前邊的線程都放棄了只是瞎站着,那也說不定,對吧!

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
      int ws = pred.waitStatus;//拿到前驅的狀態
      if (ws == Node.SIGNAL)
          //如果已經告訴前驅拿完號後通知自己一下,那就可以安心休息了
          return true;
      if (ws > 0) {
          /*
           * 如果前驅放棄了,那就一直往前找,直到找到最近一個正常等待的狀態,並排在它的後邊。
           * 注意:那些放棄的結點,由於被自己“加塞”到它們前邊,它們相當於形成一個無引用鏈,稍後就會被保安大叔趕走了(GC回收)!
          */
         do {
             node.prev = pred = pred.prev;
         } while (pred.waitStatus > 0);
         pred.next = node;
     } else {
          //如果前驅正常,那就把前驅的狀態設置成SIGNAL,告訴它拿完號後通知自己一下。有可能失敗,人家說不定剛剛釋放完呢!
         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
     }
     return false;
 }

 

整個流程中,如果前驅結點的狀態不是SIGNAL,那麼自己就不能安心去休息,需要去找個安心的休息點,同時可以再嘗試下看有沒有機會輪到自己拿號。

3.1.3.2 parkAndCheckInterrupt()

  如果線程找好安全休息點後,那就可以安心去休息了。此方法就是讓線程去休息,真正進入等待狀態。

private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);//調用park()使線程進入waiting狀態
     return Thread.interrupted();//如果被喚醒,查看自己是不是被中斷的。
 }

   park()會讓當前線程進入waiting狀態。在此狀態下,有兩種途徑可以喚醒該線程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()會清除當前線程的中斷標記位。 

3.1.3.3 小結

  OK,看了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),現在讓我們再回到acquireQueued(),總結下該函數的具體流程:

  1. 結點進入隊尾後,檢查狀態,找到安全休息點;
  2. 調用park()進入waiting狀態,等待unpark()或interrupt()喚醒自己;
  3. 被喚醒後,看自己是不是有資格能拿到號。如果拿到,head指向當前結點,並返回從入隊到拿到號的整個過程中是否被中斷過;如果沒拿到,繼續流程1。

 

3.1.4 小結

  OKOK,acquireQueued()分析完之後,我們接下來再回到acquire()!再貼上它的源碼吧:

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

再來總結下它的流程吧:

  1. 調用自定義同步器的tryAcquire()嘗試直接去獲取資源,如果成功則直接返回;
  2. 沒成功,則addWaiter()將該線程加入等待隊列的尾部,並標記爲獨佔模式;
  3. acquireQueued()使線程在等待隊列中休息,有機會時(輪到自己,會被unpark())會去嘗試獲取資源。獲取到資源後才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false。
  4. 如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源後纔再進行自我中斷selfInterrupt(),將中斷補上。

由於此函數是重中之重,我再用流程圖總結一下:

至此,acquire()的流程終於算是告一段落了。這也就是ReentrantLock.lock()的流程,不信你去看其lock()源碼吧,整個函數就是一條acquire(1)!!!

3.2 release(int)

   上一小節已經把acquire()說完了,這一小節就來講講它的反操作release()吧。此方法是獨佔模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果徹底釋放了(即state=0),它會喚醒等待隊列裏的其他線程來獲取資源。這也正是unlock()的語義,當然不僅僅只限於unlock()。下面是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;
 }

  邏輯並不複雜。它調用tryRelease()來釋放資源。有一點需要注意的是,它是根據tryRelease()的返回值來判斷該線程是否已經完成釋放掉資源了!所以自定義同步器在設計tryRelease()的時候要明確這一點!!

3.2.1 tryRelease(int)

  此方法嘗試去釋放指定量的資源。下面是tryRelease()的源碼:

protected boolean tryRelease(int arg) { 
 throw new UnsupportedOperationException(); 
}

  跟tryAcquire()一樣,這個方法是需要獨佔模式的自定義同步器去實現的。正常來說,tryRelease()都會成功的,因爲這是獨佔模式,該線程來釋放資源,那麼它肯定已經拿到獨佔資源了,直接減掉相應量的資源即可(state-=arg),也不需要考慮線程安全的問題。但要注意它的返回值,上面已經提到了,release()是根據tryRelease()的返回值來判斷該線程是否已經完成釋放掉資源了!所以自義定同步器在實現時,如果已經徹底釋放資源(state=0),要返回true,否則返回false。

3.2.2 unparkSuccessor(Node)

  此方法用於喚醒等待隊列中下一個線程。下面是源碼:

 private void unparkSuccessor(Node node) {
      //這裏,node一般爲當前線程所在的結點。
      int ws = node.waitStatus;
      if (ws < 0)//置零當前線程所在的結點狀態,允許失敗。
          compareAndSetWaitStatus(node, ws, 0);
  
      Node s = node.next;//找到下一個需要喚醒的結點s
      if (s == null || s.waitStatus > 0) {//如果爲空或已取消
          s = null;
         for (Node t = tail; t != null && t != node; t = t.prev)
             if (t.waitStatus <= 0)//從這裏可以看出,<=0的結點,都是還有效的結點。
                 s = t;
     }
     if (s != null)
         LockSupport.unpark(s.thread);//喚醒
 }

 

  這個函數並不複雜。一句話概括:用unpark()喚醒等待隊列中最前邊的那個未放棄線程,這裏我們也用s來表示吧。此時,再和acquireQueued()聯繫起來,s被喚醒後,進入if (p == head && tryAcquire(arg))的判斷(即使p!=head也沒關係,它會再進入shouldParkAfterFailedAcquire()尋找一個安全點。這裏既然s已經是等待隊列中最前邊的那個未放棄線程了,那麼通過shouldParkAfterFailedAcquire()的調整,s也必然會跑到head的next結點,下一次自旋p==head就成立啦),然後s把自己設置成head標杆結點,表示自己已經獲取到資源了,acquire()也返回了!!And then, DO what you WANT!

3.2.3 小結

  release()是獨佔模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果徹底釋放了(即state=0),它會喚醒等待隊列裏的其他線程來獲取資源。

3.3 acquireShared(int)

  此方法是共享模式下線程獲取共享資源的頂層入口。它會獲取指定量的資源,獲取成功則直接返回,獲取失敗則進入等待隊列,直到獲取到資源爲止,整個過程忽略中斷。下面是acquireShared()的源碼:

 public final void acquireShared(int arg) {
     if (tryAcquireShared(arg) < 0)
         doAcquireShared(arg);
}

       這裏tryAcquireShared()依然需要自定義同步器去實現。但是AQS已經把其返回值的語義定義好了:負值代表獲取失敗;0代表獲取成功,但沒有剩餘資源;正數表示獲取成功,還有剩餘資源,其他線程還可以去獲取。所以這裏acquireShared()的流程就是:

  1. tryAcquireShared()嘗試獲取資源,成功則直接返回;
  2. 失敗則通過doAcquireShared()進入等待隊列,直到獲取到資源爲止才返回。

3.3.1 doAcquireShared(int)

  此方法用於將當前線程加入等待隊列尾部休息,直到其他線程釋放資源喚醒自己,自己成功拿到相應量的資源後才返回。下面是doAcquireShared()的源碼:

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) {//如果到head的下一個,因爲head是拿到資源的線程,此時node被喚醒,很可能是head用完資源來喚醒自己的
                  int r = tryAcquireShared(arg);//嘗試獲取資源
                 if (r >= 0) {//成功
                     setHeadAndPropagate(node, r);//將head指向自己,還有剩餘資源可以再喚醒之後的線程
                     p.next = null; // help GC
                     if (interrupted)//如果等待過程中被打斷過,此時將中斷補上。
                         selfInterrupt();
                     failed = false;
                     return;
                 }
             }
             
             //判斷狀態,尋找安全點,進入waiting狀態,等着被unpark()或interrupt()
             if (shouldParkAfterFailedAcquire(p, node) &&
                 parkAndCheckInterrupt())
                 interrupted = true;
         }
     } finally {
         if (failed)
             cancelAcquire(node);
     }
 }

  有木有覺得跟acquireQueued()很相似?對,其實流程並沒有太大區別。只不過這裏將補中斷的selfInterrupt()放到doAcquireShared()裏了,而獨佔模式是放到acquireQueued()之外,其實都一樣,不知道Doug Lea是怎麼想的。

  跟獨佔模式比,還有一點需要注意的是,這裏只有線程是head.next時(“老二”),纔會去嘗試獲取資源,有剩餘的話還會喚醒之後的隊友。那麼問題就來了,假如老大用完後釋放了5個資源,而老二需要6個,老三需要1個,老四需要2個。老大先喚醒老二,老二一看資源不夠,他是把資源讓給老三呢,還是不讓?答案是否定的!老二會繼續park()等待其他線程釋放資源,也更不會去喚醒老三和老四了。獨佔模式,同一時刻只有一個線程去執行,這樣做未嘗不可;但共享模式下,多個線程是可以同時執行的,現在因爲老二的資源需求量大,而把後面量小的老三和老四也都卡住了。當然,這並不是問題,只是AQS保證嚴格按照入隊順序喚醒罷了(保證公平,但降低了併發)。

 

3.3.1.1 setHeadAndPropagate(Node, int)

private void setHeadAndPropagate(Node node, int propagate) {
      Node h = head; 
      setHead(node);//head指向自己
       //如果還有剩餘量,繼續喚醒下一個鄰居線程
      if (propagate > 0 || h == null || h.waitStatus < 0) {
          Node s = node.next;
          if (s == null || s.isShared())
              doReleaseShared();
      }
 }

  此方法在setHead()的基礎上多了一步,就是自己甦醒的同時,如果條件符合(比如還有剩餘資源),還會去喚醒後繼結點,畢竟是共享模式!

  doReleaseShared()我們留着下一小節的releaseShared()裏來講。

 

3.3.2 小結

  OK,至此,acquireShared()也要告一段落了。讓我們再梳理一下它的流程:

    1. tryAcquireShared()嘗試獲取資源,成功則直接返回;
    2. 失敗則通過doAcquireShared()進入等待隊列park(),直到被unpark()/interrupt()併成功獲取到資源才返回。整個等待過程也是忽略中斷的。

  其實跟acquire()的流程大同小異,只不過多了個自己拿到資源後,還會去喚醒後繼隊友的操作(這纔是共享嘛)

3.4 releaseShared()

  上一小節已經把acquireShared()說完了,這一小節就來講講它的反操作releaseShared()吧。此方法是共享模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果成功釋放且允許喚醒等待線程,它會喚醒等待隊列裏的其他線程來獲取資源。下面是releaseShared()的源碼:

public final boolean releaseShared(int arg) {
     if (tryReleaseShared(arg)) {//嘗試釋放資源
         doReleaseShared();//喚醒後繼結點
         return true;
     }
     return false;
 }

  此方法的流程也比較簡單,一句話:釋放掉資源後,喚醒後繼。跟獨佔模式下的release()相似,但有一點稍微需要注意:獨佔模式下的tryRelease()在完全釋放掉資源(state=0)後,纔會返回true去喚醒其他線程,這主要是基於獨佔下可重入的考量;而共享模式下的releaseShared()則沒有這種要求,共享模式實質就是控制一定量的線程併發執行,那麼擁有資源的線程在釋放掉部分資源時就可以喚醒後繼等待結點。例如,資源總量是13,A(5)和B(7)分別獲取到資源併發運行,C(4)來時只剩1個資源就需要等待。A在運行過程中釋放掉2個資源量,然後tryReleaseShared(2)返回true喚醒C,C一看只有3個仍不夠繼續等待;隨後B又釋放2個,tryReleaseShared(2)返回true喚醒C,C一看有5個夠自己用了,然後C就可以跟A和B一起運行。而ReentrantReadWriteLock讀鎖的tryReleaseShared()只有在完全釋放掉資源(state=0)才返回true,所以自定義同步器可以根據需要決定tryReleaseShared()的返回值。

3.4.1 doReleaseShared()

  此方法主要用於喚醒後繼。下面是它的源碼:

 private void doReleaseShared() {
      for (;;) {
          Node h = head;
          if (h != null && h != tail) {
              int ws = h.waitStatus;
              if (ws == Node.SIGNAL) {
                  if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                      continue;
                  unparkSuccessor(h);//喚醒後繼
             }
             else if (ws == 0 &&
                      !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                 continue;
         }
         if (h == head)// head發生變化
             break;
     }
 }

3.5 小結

  本節我們詳解了獨佔和共享兩種模式下獲取-釋放資源(acquire-release、acquireShared-releaseShared)的源碼,相信大家都有一定認識了。值得注意的是,acquire()和acquireSahred()兩種方法下,線程在等待隊列中都是忽略中斷的。AQS也支持響應中斷的,acquireInterruptibly()/acquireSharedInterruptibly()即是,這裏相應的源碼跟acquire()和acquireSahred()差不多,這裏就不再詳解了。

 

四、簡單應用

  通過前邊幾個章節的學習,相信大家已經基本理解AQS的原理了。這裏再將“框架”一節中的一段話複製過來:

  不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:

  • isHeldExclusively():該線程是否正在獨佔資源。只有用到condition才需要去實現它。
  • tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
  • tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
  • tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
  • tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回false。

  OK,下面我們就以AQS源碼裏的Mutex爲例,講一下AQS的簡單應用。

4.1 Mutex(互斥鎖)

  Mutex是一個不可重入的互斥鎖實現。鎖資源(AQS裏的state)只有兩種狀態:0表示未鎖定,1表示鎖定。下邊是Mutex的核心源碼:

  class Mutex implements Lock, java.io.Serializable {
      // 自定義同步器
      private static class Sync extends AbstractQueuedSynchronizer {
          // 判斷是否鎖定狀態
          protected boolean isHeldExclusively() {
              return getState() == 1;
          }
  
          // 嘗試獲取資源,立即返回。成功則返回true,否則false。
         public boolean tryAcquire(int acquires) {
             assert acquires == 1; // 這裏限定只能爲1個量
             if (compareAndSetState(0, 1)) {//state爲0才設置爲1,不可重入!
                 setExclusiveOwnerThread(Thread.currentThread());//設置爲當前線程獨佔資源
                 return true;
             }
            return false;
         }
 
         // 嘗試釋放資源,立即返回。成功則爲true,否則false。
         protected boolean tryRelease(int releases) {
             assert releases == 1; // 限定爲1個量
             if (getState() == 0)//既然來釋放,那肯定就是已佔有狀態了。只是爲了保險,多層判斷!
                 throw new IllegalMonitorStateException();
             setExclusiveOwnerThread(null);
             setState(0);//釋放資源,放棄佔有狀態
             return true;
         }
     }
 
     // 真正同步類的實現都依賴繼承於AQS的自定義同步器!
     private final Sync sync = new Sync();
 
     //lock<-->acquire。兩者語義一樣:獲取資源,即便等待,直到成功才返回。
     public void lock() {
         sync.acquire(1);
     }
 
     //tryLock<-->tryAcquire。兩者語義一樣:嘗試獲取資源,要求立即返回。成功則爲true,失敗則爲false。
    public boolean tryLock() {
         return sync.tryAcquire(1);
     }
 
     //unlock<-->release。兩者語文一樣:釋放資源。
     public void unlock() {
         sync.release(1);
     }
 
     //鎖是否佔有狀態
     public boolean isLocked() {
         return sync.isHeldExclusively();
     }
 }

 

  同步類在實現時一般都將自定義同步器(sync)定義爲內部類,供自己使用;而同步類自己(Mutex)則實現某個接口,對外服務。當然,接口的實現要直接依賴sync,它們在語義上也存在某種對應關係!!而sync只用實現資源state的獲取-釋放方式tryAcquire-tryRelelase,至於線程的排隊、等待、喚醒等,上層的AQS都已經實現好了,我們不用關心。

  除了Mutex,ReentrantLock/CountDownLatch/Semphore這些同步類的實現方式都差不多,不同的地方就在獲取-釋放資源的方式tryAcquire-tryRelelase。掌握了這點,AQS的核心便被攻破了!

  

       下面將從實現角度分析AQS是如何完成線程同步,主要包括:同步隊列、獨佔式同步狀態獲取與釋放、共享式同步狀態獲取與釋放、超時獲取同步狀態等AQS的核心數據結構模板方法。

同步隊列

AQS依賴同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理。當前線程獲取同步狀態失敗時,AQS會將當前線程以及等待狀態等信息構造成一個節點(Node)並且將其加入到同步隊列中,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。

同步隊列中的Node節點用來保存獲取同步狀態失敗的線程引用。等待狀態以及前驅和後繼節點。

 

    static final class Node {
        /** 表示節點正處在共享模式下等待的標記 **/
        static final Node SHARED = new Node();
        /**表示節點正在以獨佔模式等待的標記*/
        static final Node EXCLUSIVE = null;
        /**waitStatus值,表示線程已取消 */
        static final int CANCELLED =  1;
        /** waitStatus值,表示後繼線程需要取消掛起 */
        static final int SIGNAL    = -1;
        /** waitStatus值,表示線程正在等待條件 */
        static final int CONDITION = -2;
        /**waitStatus值指示下一個acquireShared應無條件傳播*/
        static final int PROPAGATE = -3;

        /**
         * 狀態字段,僅接受值:
         * 
         * SIGNAL:值爲-1 ,後繼節點的線程處於等待狀態,
         * 而當前節點的線程如果釋放了同步狀態或者被取消,
         * 將會通知後繼節點,使後繼節點的線程得以運行。
         *     
         * CANCELLED:值爲1,由於在同步隊列中等待的
         * 線程等待超時或者被中斷,需要從同步隊列中取消等待,
         * 節點進入該狀態將不會變化
         *
         * CONDITION: 值爲-2,節點在等待隊列中,
         * 節點線程等待在Condition上,當其他線程
         * 對Condition調用了singal方法後,該節點
         * 將會從等待隊列中轉移到同步隊列中,加入到
         * 對同步狀態的獲取中
         *
         * PROPAGATE: 值爲-3,表示下一次共享模式同步
         * 狀態獲取將會無條件地傳播下去
         * 
         * INITIAL: 初始狀態值爲0 
         */
        volatile int waitStatus;

        /**
        * 鏈接到前驅節點,當前節點/線程依賴它來檢查waitStatus。
        * 在入同步隊列時被設置,並且僅在移除同步隊列時才歸零
        * (爲了GC的目的)。 此外,在取消前驅節點時,我們在找到
        * 未取消的一個時進行短路,這將始終存在,因爲頭節點從未
        * 被取消:節點僅作爲成功獲取的結果而變爲頭。
        * 被取消的線程永遠不會成功獲取,並且線程只取消自身,
        * 而不是任何其他節點。
        */
        volatile Node prev;

        /**
        * 鏈接到後續節點,當前節點/線程釋放時釋放。
        * 在入同步隊列期間分配,在繞過取消的前驅節
        * 點時調整,並在出同步隊列時取消(爲了GC的目的)。
        * enq操作不會分配前驅節點的next字段,直到附加之後,
        * 因此看到一個爲null的next字段不一定意味着該節點在
        * 隊列的末尾。 但是,如果next字段顯示爲null,我們
        * 可以從尾部掃描prev,仔細檢查。 被取消的節點的next字段
        * 被設置爲指向節點本身而不是null,以使isOnSyncQueue更
        * 方便操作。調用isOnSyncQueue時,如果節點(始終
        * 是放置在條件隊列上的節點)正等待在同步隊列上重新獲取,則返回true。
        **/
        volatile Node next;
        /**
        * 將此節點入列的線程。在構造方法裏初始化,使用後清零。
        * 鏈接到下一個節點等待條件,或特殊值SHARED。
        * 因爲條件隊列只有在保持在獨佔模式時才被訪問,
        * 所以我們只需要一個簡單的鏈接隊列來保存節點,
        * 同時等待條件。 然後將它們轉移到隊列中以重新獲取。
        * 並且因爲條件只能是排它的,我們通過使用特殊的
        * 值來指示共享模式來保存一個字段。
        */

        Node nextWaiter;

        /**
         *如果節點在共享模式下等待,則返回true
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

      /**
      * 返回上一個節點,如果爲null,
      * 則拋出NullPointerException。當前驅節點不爲null時使用。
      **/
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
        /**
        *用於建立初始化head節點或SHARED標記
        **/
        Node() {  

        }
        /**
        *由addWaiter使用
        **/
        Node(Thread thread, Node mode) {
            this.nextWaiter = mode;
            this.thread = thread;
        }
        /**
        * 供Condition使用
        */
        Node(Thread thread, int waitStatus) {
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

 

Node是構成同步隊列的基礎,AQS擁有首節點(head)和尾節點(tail),沒有成功獲取同步狀態的線程將會放入到隊列的尾部

同步隊列的基本結構:

 

這裏寫圖片描述

 

同步器中包含了兩個節點類型的引用,一個指向頭節點(head),一個指向尾節點(tail),沒有獲取到鎖的線程,加入到隊列的過程必須保證線程安全,因此同步器提供了一個基於CAS的設置尾節點的方法CompareAndSetTail(Node expect,Node update),它需要傳遞當前線程認爲的尾節點和當前節點,只有設置成功後,當前節點才能正式與之前的尾節點建立關聯。

 

同步器將節點加入到同步隊列的過程如圖所示: 
這裏寫圖片描述

 

同步隊列遵循FIFO,首節點是獲取鎖成功的節點,首節點的線程在釋放鎖時,將會喚醒後繼節點,而後繼節點將會在獲取到鎖時,將自己設置位首節點,過程如下所示: 
這裏寫圖片描述

 

設置首節點是由成功獲取鎖的線程來完成的,由於只有一個線程能夠成功獲取鎖,因此設置首節點不需要CAS操作。

AQS同步狀態獲取與釋放

1. 獨佔式

2. 共享式

3. 獨佔式超時獲取

獨佔式同步狀態的獲取與釋放

通過調用AQS的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不起作用,即由於線程獲取同步狀態失敗後進入同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移出。

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

 

該方法主要的邏輯是:首先調用自定義同步器實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(獨佔式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態)並通過addWaiter(Node node)方法將節點加入到同步隊列的尾部,最後調用acquireQueued(Node node,int arg)方法,使得節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        //快速嘗試在尾部添加節點
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;//如果尾部節點存在,那麼新加入到隊列尾部節點的前驅節點爲pred
            if (compareAndSetTail(pred, node)) {//通過CAS操作設置尾部節點
                pred.next = node;//pred的後續節點爲先加入的尾部節點
                return node;
            }
        }
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 說明隊列爲null,必須初始化
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

 

以上代碼,通過compareAndSetTail(Node expect,Node update)方法來確保節點能夠被線程安全的添加到同步隊列的尾部。在enq(final Node node)方法中,同步器通過“死循環”來保證節點的正確添加,在死循環中,只有通過CAS將節點設置爲尾節點後,當前線程才能從該方法返回,否則,當前線程不斷得嘗試設置。enq(final Node node)方法將併發添加節點的請求通過CAS變得串行化了。

節點進入到同步隊列後,進入了一個自旋的過程,每個節點都在自省的觀察,當條件滿足,獲取到了同步狀態時,就可以從這個自旋中退出,否則繼續自旋並且阻塞節點的線程

    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);//將節點設置爲頭節點
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //檢查和更新未能獲取的節點的狀態。
      //如果線程應該阻塞,返回true。 這是主要信號
      //控制所有獲取循環。 需要pred == node.prev
                if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()
                    interrupted = true;
            }
        } finally {
            if (failed)//如果需要中斷,則把節點從隊列中移除
                cancelAcquire(node);
        }
    }

 

從以上代碼可以知道,只有當線程的前驅節點是頭節點才能繼續獲取同步狀態,原因如下:

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

2. 維護同步隊列的FIFO原則。

獨佔式同步狀態獲取流程如下:

 

這裏寫圖片描述

 

當前線程獲取同步狀態並且執行完了對應的邏輯後,需要釋放同步狀態,使得後續節點繼續獲取同步狀態,通過調用release(int arg)方法來實現。該方法不但釋放同步狀態,還可以喚醒後繼節點重新獲取同步狀態

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

 

總結一下:在獲取同步狀態時,AQS維護這一個同步隊列,獲取狀態失敗的線程,會先創建一個獨佔式節點,並且加入到同步隊列的尾部,同時在同步隊列中進行自旋;移除隊列的條件是前驅節點爲頭結點並且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然後喚醒頭節點的後繼節點。

共享式獲取同步狀態

共享式獲取與獨佔式獲取的最主要區別在於同一時刻能否有多個線程同時獲取到同步狀態。通過調用acquireShared(int arg)方法可以共享式得獲取同步狀態。

    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
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 

在acquireShared(int arg)方法中,同步器調用tryAcquireShared(int arg)方法嘗試獲取同步狀態,其返回值爲int類型,當返回值大於0時,表示能夠獲取同步狀態。因此,在共享式獲取的自旋過程中,成功獲取同步狀態並且退出自旋的條件就是tryAcquireShared(int arg)方法返回值大於等於0。共享式釋放同步狀態狀態是通過調用releaseShared(int arg)方法

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

 

該方法與獨佔式主要區別在於tryReleaseShared(int arg)方法必須確保同步狀態線程安全釋放。一般是通過循環和CAS來保證的。因爲釋放同步狀態的操作會同時來自多個線程。

這裏寫圖片描述

 

通過截圖我們可以知道,CountDownLatch、ReentrantReadWriteLock、Semaphore等都是共享式獲取同步狀態的。

獨佔式超時獲取同步狀態

通過調用同步器的doAcquireNanos(int arg,long nanosTimeOut)方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則,返回false。

doAcquireNanos(int arg,long nanosTimeOut)方法針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,爲了防止過早同時,nanosTimeout計算公式爲:nanosTimeout -=now-lastTime,其中now爲當前喚醒時間,lastTime爲上次喚醒時間,如果nanosTimeout大於0則表示超時時間未到,反之,則表示超時。

 private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
        long lastTime = System.nanoTime();
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                if (nanosTimeout <= 0)//超時
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)//spinForTimeoutThreshold=1000L
                    LockSupport.parkNanos(this, nanosTimeout);//
                long now = System.nanoTime();
                nanosTimeout -= now - lastTime;
                lastTime = now;
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 

該方法在自旋時,當節點的前驅節點爲頭節點時嘗試獲取同步狀態,如果獲取成功則從方法返回,這個過程和獨佔式獲取過程相似,但是在獲取失敗時,有所不同。如果當前線程獲取同步狀態失敗時,則判斷是否超時(nanosTimeout小於等於0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然後是當前線程等待nanosTimeout納秒,當已到設置的超時時間,該線程會從LockSupport.parkNanos(Object blocker,long nanos)方法返回。

如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行超時等待,而是進入快速的自旋過程。因爲,非常短的超時等待無法做到十分精確,如果這時在進行超時等待,相反會讓nanosTimeout的超時從整體上表現的反而 不精確。因此,在超市分長短的場景下,同步器會進入無條件的快速自旋。

獨佔式超時獲取同步狀態的流程 è¿éåå¾çæè¿°

 

 

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