併發編程系列之Condition接口

前言

前面我們學習線程的時候講過等待通知模式,之前講的是通過wait,notify/notifyAll配合synchronized關鍵字,實現等待通知,今天呢,我們介紹另外一種同樣實現等待通知模式的對象叫做condition接口,配合lock使用,也能完成等待通知,但是跟之前說的又有一些區別,今天就讓我們來認識一下吧,OK,開始我們今天的併發之旅吧,祝您旅途愉快。

 

什麼是Condition接口?

Condition定義了等待/通知兩種類型的方法,當前線程如果調用這些方法之前,必須先獲取到condition對象關聯的鎖,Condition對象是由Lock對象創建出來的,也就是說Condition是綁定在一個Lock對象上的,依賴於Lock對象,使用時需要通過Lock對象new出來。相比於之前的wait/notify而言,condition充當wait/notify的角色,而lock對象充當synchronized鎖角色。

我們看下Condition接口提供的一些方法如下:

 

 

如何使用Condition實現等待通知

案例1:一個condition即1個等待隊列

通過上面我們知道要使用condition提供的方法就必須將它綁定一個Lock對象,然後用法和wait/notify差不多,我們看下面demo

public class ConditionDemo {
   // 創建重入鎖
   private Lock lock = new ReentrantLock();
   // Lock對象創建condition對象
   private Condition condition = lock.newCondition();

   public void method1(){
       try {
           lock.lock();
           System.out.println("當前線程:" + Thread.currentThread().getName() + "獲取鎖,並睡眠3秒..");
           Thread.sleep(3000);
           System.out.println("當前線程:" + Thread.currentThread().getName() + "釋放鎖..進入等待狀態");
           // 等待
           condition.await();
           System.out.println("當前線程:" + Thread.currentThread().getName() +"被喚醒,繼續執行...");
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           lock.unlock();
       }
   }

   public void method2(){
       try {
           lock.lock();
           System.out.println("當前線程:" + Thread.currentThread().getName() + "獲取鎖,進入...睡眠3秒");
           Thread.sleep(3000);
           System.out.println("當前線程:" + Thread.currentThread().getName() + "發出喚醒..,並釋放鎖");
           // 喚醒,同樣要注意多線程下死鎖的發生,優先使用signalAll()喚醒所有等待此條件的線程
           condition.signal();
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           lock.unlock();
       }
   }

   public static void main(String[] args) throws Exception{
       final ConditionDemo uc = new ConditionDemo();
       Thread t1 = new Thread(new Runnable() {
           @Override
           public void run() {
               uc.method1();
           }
       }, "線程1");
       Thread t2 = new Thread(new Runnable() {
           @Override
           public void run() {
               uc.method2();
           }
       }, "線程2");
       System.out.println("線程1啓動。。。");
       t1.start();
       Thread.sleep(1000);
       System.out.println("線程2啓動。。。");
       t2.start();
   }
}

執行結果如下:

案例2:多個Condition,即多個等待隊列

public class ConditionDemo {

   // 定義重入鎖
   private ReentrantLock lock = new ReentrantLock();
   // condition1
   private Condition c1 = lock.newCondition();
   // condition2
   private Condition c2 = lock.newCondition();

   public void m1(){
       try {
           lock.lock();
           System.out.println("當前線程:" +Thread.currentThread().getName() + "進入方法m1等待..");
           c1.await();
           System.out.println("當前線程:" +Thread.currentThread().getName() + "方法m1繼續..");
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           lock.unlock();
       }
   }

   public void m2(){
       try {
           lock.lock();
           System.out.println("當前線程:" +Thread.currentThread().getName() + "進入方法m2等待..");
           c1.await();
           System.out.println("當前線程:" +Thread.currentThread().getName() + "方法m2繼續..");
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           lock.unlock();
       }
   }

   public void m3(){
       try {
           lock.lock();
           System.out.println("當前線程:" +Thread.currentThread().getName() + "進入方法m3等待..");
           c2.await();
           System.out.println("當前線程:" +Thread.currentThread().getName() + "方法m3繼續..");
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           lock.unlock();
       }
   }

   public void m4(){
       try {
           lock.lock();
           System.out.println("當前線程:" +Thread.currentThread().getName() + "喚醒..");
           // 喚醒所有condition1上的等待線程
           c1.signalAll();
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           lock.unlock();
       }
   }

   public void m5(){
       try {
           lock.lock();
           System.out.println("當前線程:" +Thread.currentThread().getName() + "喚醒..");
           // 喚醒所有condition2上的等待線程
           c2.signalAll();
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           lock.unlock();
       }
   }

   public static void main(String[] args) {
       final ConditionDemo umc = new ConditionDemo();
       Thread t1 = new Thread(new Runnable() {
           @Override
           public void run() {
               umc.m1();
           }
       },"Thread1");
       Thread t2 = new Thread(new Runnable() {
           @Override
           public void run() {
               umc.m2();
           }
       },"Thread2");
       Thread t3 = new Thread(new Runnable() {
           @Override
           public void run() {
               umc.m3();
           }
       },"Thread3");
       Thread t4 = new Thread(new Runnable() {
           @Override
           public void run() {
               umc.m4();
           }
       },"Thread4");
       Thread t5 = new Thread(new Runnable() {
           @Override
           public void run() {
               umc.m5();
           }
       },"Thread5");

       // condition1
       t1.start();
       // condition1
       t2.start();
       // condition2
       t3.start();
       try {
           Thread.sleep(2000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       // 喚醒condition1
       t4.start();
       try {
           Thread.sleep(2000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       // 喚醒condition2
       t5.start();
   }
}

執行結果如下:

 

Condition底層實現

Condition接口源碼中提供的是如下這幾個方法,並沒有具體實現,

public interface Condition {

   void await() throws InterruptedException;
   void awaitUninterruptibly();
   long awaitNanos(long nanosTimeout) throws InterruptedException;
   boolean await(long time, TimeUnit unit) throws InterruptedException;
   boolean awaitUntil(Date deadline) throws InterruptedException;
   void signal();
   void signalAll();
}

具體實現主要主要體現在ConditionObject這個類上,這個類是AQS的內部類,每個ConditionObject對象都包含一個等待隊列,下面我們就分別從等待隊列,等待,通知三個方面分析ConditionObject的底層實現原理

 

等待隊列

一個condition包含一個等待隊列(FIFO),condition擁有一個首節點和一個尾節點,如下圖:condition中擁有首位節點的引用,當新增節點時,只需要將原來尾節點的下個節點指向它,並且更新尾節點接口,該更新操作跟之前同步隊列中CAS更新尾節點不同,此處不需要CAS操作,因爲condition的操作是在一個lock裏面進行的,是已經獲取鎖的,所以這個操作是線程安全的;

在前面所講的wait等待通知模型中其實同步器是隻有一個同步隊列和一個等待隊列的,而在我們這裏,因爲可以同時多個condition,上面案例也有使用過,也就是說,condition實現的同步器中,其實是一個同步隊列和多個等待隊列,從condition是AQS一個內部類也可以證實這一點,也就是說我們可以創建多個condition,每個condition都可以訪問AQS提供的方法,相當於每個condition都持有所屬AQS的引用,其關係模型如下:

 

 

等待

調用Condition的await開頭的系列方法,會使當前線程進入等待隊列等待並釋放鎖,線程狀態變爲等待狀態,已上圖爲例,就是同步隊列中的首節點(或者獲取鎖的節點,因爲非公平性鎖就不一定是首節點)移動到了Condition的等待隊列中,這裏關鍵的就是等待方法await,我們來看下其源碼實現:

public final void await() throws InterruptedException {
           if (Thread.interrupted())
               throw new InterruptedException();
           // 當前線程加入等待隊列,並不是直接加入,而是把當前線程構造成一個新的節點再加入
           Node node = addConditionWaiter();
           // 釋放同步狀態即釋放鎖
           int savedState = fullyRelease(node);
           int interruptMode = 0;
           // 喚醒同步隊列中後續節點,線程進入等待
           while (!isOnSyncQueue(node)) {
               LockSupport.park(this);
               if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                   break;
           }
           // 調用acquireQueued嘗試獲取同步狀態,獲取成功後,線程中斷返回
           if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
               interruptMode = REINTERRUPT;
           if (node.nextWaiter != null) // clean up if cancelled
               unlinkCancelledWaiters();
           if (interruptMode != 0)
               reportInterruptAfterWait(interruptMode);
       }

整個過程示意圖如下:

 

通知

調用Condition的signal()方法將會喚醒在等待隊列中的首節點,該節點也是到目前爲止等待時間最長的節點,等待隊列遵循FIFO原則。調用signalAll()方法將會喚醒該同步器上等待隊列中的所有節點,我們看signal方法源碼分析:

public final void signal() {
           // 前置檢查,判斷當前線程是否是獲取了鎖的線程,如果不是拋出異常
           if (!isHeldExclusively())
               throw new IllegalMonitorStateException();
           // 取得等待隊列的頭結點,頭結點不爲空執行doSignal,否則,喚醒結束
           Node first = firstWaiter;
           if (first != null)
               doSignal(first);
       }

我們再來看下doSignal的源碼:

private void doSignal(Node first) {
           // 調用transferForSignal將節點從等待隊列移動到同步隊列
           // 將該節點從等待隊列刪除
           do {
               if ( (firstWaiter = first.nextWaiter) == null)
                   lastWaiter = null;
               first.nextWaiter = null;
           } while (!transferForSignal(first) &&
                    (first = firstWaiter) != null);
       }

再往下看,我們追溯到transferForSignal方法:

final boolean transferForSignal(Node node) {
       // 將節點waitStatus設置爲0
       if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
           return false;
       // 調用enq方法將該節點加入同步隊列
       Node p = enq(node);
       int ws = p.waitStatus;
       if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
           // 使用LockSuppor.unpark()方法喚醒該節點的線程
           LockSupport.unpark(node.thread);
       return true;
   }

signalAll()方法實現也差不多,我就不做過多講解了,看下doSignlAll方法即可:

/**
  * Removes and transfers all nodes.
  * @param first (non-null) the first node on condition queue
 */
private void doSignalAll(Node first) {
  lastWaiter = firstWaiter = null;
   do {
       Node next = first.nextWaiter;
       first.nextWaiter = null;
       transferForSignal(first);
       first = next;
      } while (first != null);
  }

我們再來看下節點在隊列中的變化過程,如下圖所示:

 

以上就是今天等待通知機制condition的全部內容,結合wait/notify+synchronized,對比學習,希望能對您有所收穫!!!

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