如何理解Condition

在JDK1.5以後併發包中提供了鎖定接口,條件接口與鎖配合使用可以實現等待/通知模式,在此之前是使用定義在對象對象上的一組監視器方法,主要包括:等待() wait(long timeout),notify()以及notifyAll()方法,這些方法同步結合使用,也可以實現等待/通知。


對象的監視器方法與條件接口的對比如下(圖片截取自Java的併發編程的藝術)

條件演示

public class ConditionTest {
   Lock lock = new ReentrantLock();
   Condition condition = lock.newCondition();

   public void conditionWait() throws InterruptedException {
       lock.lock();
       try {
           System.out.println(Thread.currentThread());
           condition.await();
           System.out.println("await");
       } finally {
           lock.unlock();
       }
   }

   public void conditionSignal() throws InterruptedException {
       lock.lock();
       try {
           System.out.println(Thread.currentThread());
           condition.signal();
           System.out.println("siganl");
       } finally {
           lock.unlock();
       }
   }


   public static void main(String[] args) throws InterruptedException {
       ConditionTest conditionTest = new ConditionTest();
       new Thread(new Runnable() {
           @Override
           public void run() {
               try {
                   conditionTest.conditionWait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }) {
           public String toString() {
               return getName();
           }
       }.start();

       new Thread(new Runnable() {
           @Override
           public void run() {
               try {
                   conditionTest.conditionSignal();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }) {
           public String toString() {
               return getName();
           }
       }.start();
   }
}

運行結果:

從上面的演示比較容易得出:

1,一般都會將條件對象作爲成員變量。

2,線程1調用等待()方法後當前線程會釋放鎖並等待。

3,線程線程1調用信號()方法通知線程0,線程0從等待()返回,在返回前已經獲取到了鎖。


上面demo中lock.newCondition()其實返回的是Condition的一個實現:AQS中的ConditionObject

public class ConditionObject implements Condition, java.io.Serializable {
   private static final long serialVersionUID = 1173984872572414699L;
   //這裏Condition維護了自己的等待隊列
   /** First node of condition queue. */
   private transient Node firstWaiter;
   /** Last node of condition queue. */
   private transient Node lastWaiter;

  ...省略後續的代碼...


一個的ConditionObject包含一個等待隊列,的ConditionObject包括首節點和尾節點,等待隊列是一個FIFO隊列,如果一個線程調用Condition.await()方法,那麼該線程將會釋放鎖,構造成節點加入等待隊列並進入等待狀態,並將該節點從尾部加入等待隊列。節點的定義複用了同步器中節點的定義,也就是說,同步隊列和等待隊列中節點類型都是同步器的靜態內部類AbstractQueuedSynchronizer.Node。

等待隊列的基本結構如圖,同步隊列的結構圖可以參見AQS與簡介源碼分析



條件接口提供瞭如下方法:

public interface Condition {

   //當前線程進入等待直到被通知或者中斷
   void await() throws InterruptedException;

   //跟上面的區別是對中斷不敏感
   void awaitUninterruptibly();

   //當前線程進入等待狀態直到被通知,返回值表示剩餘時間,如果在nanosTimeout納秒之前被喚醒
   //那麼返回值就是nanosTimeout-實際時間,如果返回值是0或者是負數表示已經超時
   long awaitNanos(long nanosTimeout) throws InterruptedException;

   //當前線程進入等待狀態直到被通知,中斷或者經過指定的等待時間,在等待時間之前被喚醒返回true,時間到了則返回false,可以自定義超時時間的單位
   boolean await(long time, TimeUnit unit) throws InterruptedException;

   //當前線程進入等待狀態直到被通知,中斷或者到某個時間,如果沒有到指定時間就被通知,返回true,否則到達指定時間返回false
   boolean awaitUntil(Date deadline) throws InterruptedException;

   //喚醒一個等待在Condition上的線程,該方法從等待方法返回前必須獲得與Contion相關聯的鎖
   void signal();

   //喚醒所有等待在Condition上的線程,能夠從等待方法返回前必須獲得與Contion相關聯的鎖
   void signalAll();
}


等待

調用條件的等待()方法(或者以AWAIT開頭的方法),會使當前線程進入等待隊列並釋放鎖,同時線程狀態變爲等待狀態。當從AWAIT()方法返回時,當前線程一定獲取了條件相關聯的鎖。

如果從隊列(同步隊列和等待隊列)的角度看AWAIT()方法,當調用AWAIT()方法時,相當於同步隊列的首節點(獲取了鎖的節點)移動到條件的等待隊列中。

public final void await() throws InterruptedException {
   //如果線程被中斷則拋出異常
   if (Thread.interrupted())
       throw new InterruptedException();
   //將當前線程構造成節點加入到等待隊列節點中
   Node node = addConditionWaiter();
   //釋放當前線程佔有的鎖
   int savedState = fullyRelease(node);
   int interruptMode = 0;
   //判斷當前節點是否在AQS隊列中,如果不在就繼續掛起
   while (!isOnSyncQueue(node)) {
       LockSupport.park(this);
       //如果中斷則退出循環
       if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
           break;
   }
   //競爭嘗試獲取鎖,如果沒有獲取到則繼續阻塞等待被喚醒再次競爭鎖
   if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
       interruptMode = REINTERRUPT;
   //清理Contidion維護的等待隊列
   if (node.nextWaiter != null) // clean up if cancelled
       unlinkCancelledWaiters();
   if (interruptMode != 0)
       reportInterruptAfterWait(interruptMode);
}


final boolean isOnSyncQueue(Node node) {
   //如果node節點的狀態爲CONDITION,表示節點狀態正常沒有被喚醒,此刻節點還沒有進入AQS隊列中
   if (node.waitStatus == Node.CONDITION || node.prev == null)
       return false;
   //如果當前節點的下一個節點不爲空的話,那麼該節點肯定在AQS隊列中
   if (node.next != null) // If has successor, it must be on queue
       return true;
   //循環遍歷,返回ture表示當前節點在AQS對列中
   return findNodeFromTail(node);
}


通知

調用條件的信號()方法,將會喚醒在等待隊列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移到同步隊列中。

public final void signal() {
   //判斷當前線程是否爲擁有鎖的獨佔式線程,如果不是則拋出異常
   if (!isHeldExclusively())
       throw new IllegalMonitorStateException();
   //排隊喚醒,最開始喚醒Condition維護的隊列中的頭節點
   Node first = firstWaiter;
   if (first != null)
       doSignal(first);
}


private void doSignal(Node first) {
   do {
       //如果頭節點的下一個節點爲null,則將頭節點和其尾節點置null
       if ( (firstWaiter = first.nextWaiter) == null)
           lastWaiter = null;
       first.nextWaiter = null;
   } while (!transferForSignal(first) &&
            (first = firstWaiter) != null);
}


final boolean transferForSignal(Node node) {

   //cas設置node節點從contion狀態改爲初始
   if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
       return false;
   //通過自旋的方式將節點加入到AQS隊列中
   //等待隊列中的頭節點線程安全地移動到同步隊列。當節點移動到同步隊列後,當前線程再使用LockSupport喚醒該節點的線程。
   Node p = enq(node);
   int ws = p.waitStatus;
   //如果當前節點的上個節點的waitStatus大於1則說明該節點所對應的線程等待超時或者被中斷需要從同步隊列中取消等待,如果CAS修改失敗則直接喚醒
   //if語句一般走不到這裏,喚醒的操作一般在lock.unlock()中
   if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
       LockSupport.unpark(node.thread);
   return true;
}


總結

演示中的具體流程如下:

1,線程1調用lock.lock()獲取到鎖,內部調用acquireQueued方法,線程被加入到AQS的同步隊列中。

2,線程1調用AWAIT方法時,鎖釋放,該線程鎖構成的節點從AQS的同步隊列中移除,通過addConditionWaiter()方法加入到條件的等待隊列中,等待着被通知的信號。

3,線程1釋放鎖喚醒胎面-2獲取到鎖,加入到AQS的同步隊列中,處理業務

4,Tread-2調用signal方法,Condition的等待隊列中只有Thread-1一個節點,通過調用enq(節點節點)方法加入到AQS的同步隊列中,此時Thread-1並沒有被喚醒,喚醒的操作是在finall塊中的lock.unlock()中。

5,胎面2調用lock.unLock()方法,釋放鎖,線程1被喚醒並獲取到鎖從AWAIT()方法返回繼續處理業務。

如圖6所示,線程1調用解鎖釋放鎖,結束整個流程。


參考文章:

Doug Lea:“Java併發編程實戰”

方騰飛,魏鵬,程曉明:“併發編程的藝術”


CSDN文章同步會慢些,歡迎關注微信公衆號:挨踢男孩


    



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