關於java線程淺談五: Condition條件


【CSDN 技術主題月】物聯網全棧開發      【評論送書】每週薦書:MySQL、Kafka、微信小程序      【直播】Android 老司機帶你開黑

關於java線程淺談五: Condition條件

標籤: java線程併發condition
223人閱讀 評論(0) 收藏 舉報
本文章已收錄於:
分類:

Java.util.concurrent 包在java語言中可以說是比較難啃的一塊,但理解好這個包下的知識,對學習java來說,不可謂是一種大的提升,我也嘗試着用自己不聰明的腦袋努力的慢慢啃下點東西來。其實 java.util.concurrent 包中,最核心的就是AQS( AbstractQueuedSynchronizer) 這個抽象類,可以說是整個JUC包的基石,但今天先不說AQS,我先從比較容易理解的 Condition 條件講起。

什麼是Condition條件

Condition 是定義在 java.util.concurrent.locks 包下的一個接口,這個接口主要的功能就是實現了與Object類中的wait(),notify()方法相同的語義,但是功能上更強大。Condition 接口中定義的方式其實很少,列舉下來:

    //使當前線程接到signal信號之前,或者被中斷之前一直處於等待狀態
    void await() throws InterruptedException;
    //使當前線程接到signal信號之前一直處於等待狀態
    void awaitUninterruptibly(); 
    //使當前線程接到signal信號之前,或到達指定等待的時間之前,或者被中斷之前一直處於等待狀態
    boolean await(long time, TimeUnit unit) throws InterruptedException; 
    //使當前線程接到signal信號之前,或到達指定的最後期限時間之前,或者被中斷之前一直處於等待狀態
    boolean awaitUntil(Date deadline) throws InterruptedException; 
   //使當前線程接到signal信號之前,或到達指定等待的時間之前,或者被中斷之前一直處於等待狀態
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    //向一個線程發送喚醒信號
    void signal(); 
    //向所有線程發送喚醒信號
    void signalAll(); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

從定義的方法中也可以看出這個類的功能,無非就兩種:等待方法、喚醒等待方法。

Condition 的使用

下面用一個之前用的小demo展示一下 Condition 的使用方法:
先使用Object中的wait,notify 方法:

public class TestCondition {

    public static void main(String[] args) {
        try {
            ThreadTest t1 = new ThreadTest("t1");
            synchronized (t1) {
                System.out.println(Thread.currentThread().getName()+"線程啓動線程 t1");
                t1.start();

                System.out.println(Thread.currentThread().getName()+"線程執行wait方法,等待被喚醒");
                t1.wait();

                System.out.println(Thread.currentThread().getName()+"線程繼續執行");
            }
        } catch (InterruptedException e) 
            e.printStackTrace();
        }
    }
}

class ThreadTest extends Thread{
    public ThreadTest(String name){
        super(name);
    }
    public void run(){
        synchronized (this) {
            System.out.println(Thread.currentThread().getName()+"線程執行 notify 方法");
            notify();   
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

運行結果:

main線程啓動線程 t1
main線程執行wait方法,等待被喚醒
t1線程執行 notify 方法
main線程繼續執行
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

這裏就不再詳述關於 synchronized 關鍵字的使用,舉這個示例是爲了引出Condition的使用。
稍微改一下上面的程序:

public class TestCondition {

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    public static void main(String[] args) {
        try {
            ThreadTest t1 = new ThreadTest("t1");
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"線程啓動線程 t1");
                t1.start();

                System.out.println(Thread.currentThread().getName()+"線程執行condition.await()方法,等待被喚醒");
                condition.await();

                System.out.println(Thread.currentThread().getName()+"線程繼續執行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }

    static class ThreadTest extends Thread{
        public ThreadTest(String name){
            super(name);
        }

        public void run(){
            try{
                lock.lock();
                System.out.println(Thread.currentThread().getName()+"線程執行condition.signal()方法");
                condition.signal();
            }finally{
                lock.unlock();
            }
        }

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

運行結果:

main線程啓動線程 t1
main線程執行condition.await()方法,等待被喚醒
t1線程執行condition.signal()方法
main線程繼續執行
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

根據上面的實例可以看出,Condition工具類是配合Lock類一起使用的,當然Condition的作用遠不止上面代碼這樣簡單,其實它最主要的作用是可以在同一把鎖上,針對不同的業務使用不用的Condition。比如在前面的文章提到的生產消費問題,我們完全可以使用兩個Condition,一個針對於生產,一個針對於消費,當產品爲0時,我們可以讓消費的Condition執行await方法,當產品不爲0時,可以讓消費的Condition執行signal方法。生產者也是類似,具體代碼這裏就不再詳述了,可以嘗試自己實現一下。

Condition 的實現類:ConditionObject

Condition 是一個接口,那它到底是怎麼工作的呢?我們來看一下ReentrantLock 類是怎麼樣使用Condition 的。

Condition condition = lock.newCondition();//這是生成Condition 的方法
  • 1
  • 1

追蹤一下newCondition( )這個方法,我們就可以看到一個Condition 的具體實現:

 public Condition newCondition() {
        return sync.newCondition(); //調用sync的newCondition
    }
//sync的方法,返回一個new ConditionObject()
final ConditionObject newCondition() {
        return new ConditionObject();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這個sync是什麼呢?它是定義在ReentrantLock 中的內部類,它繼承了上面提到的AQS這個抽象類,從某種角度來說ReentrantLock 只是提供了一個可以操作AQS這個核心類的入口,代理了一些重要的方法,那爲什麼不讓ReentrantLock 直接繼承AQS,而是選擇用一個內部類來實現呢,可能是出於一些安全性方面的考慮。ConditionObject是定義在AQS中的。

這裏寫圖片描述

這裏寫圖片描述

Condition 與 AQS 到底是怎麼工作的?

探究這個問題,就需要來看一下ConditionObject的源碼了。
代表性的,我們看一下await方法和signal方法的源碼(基於jdk1.8.0_112):
await方法:

 public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException(); //判斷線程的中斷狀態,如果中斷則拋出異常
            //將當前線程包裝成一個條件等待節點添加到ConditionObject維護的一個隊列中
            Node node = addConditionWaiter();
            //釋放當前線程佔有的鎖
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //這個while 循環就是在當前線程釋放鎖後,一直觀察持有自己線程的節點有沒有被加載到
            //AQS維護的等待隊列中(加入到這個隊列中才有獲取鎖的資格),什麼時候會加入到這個隊列中呢? 
            //當然是執行了喚醒這個線程對應的singal方法的時候啦
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在看一下signal方法:

 public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;//ConditionObject維護隊列中的頭節點,進行喚醒操作
            if (first != null)
                doSignal(first);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

從上面的敘述不難看出,AQS 和 ConditionObject各自維護了一個隊列,用來存放包裝了線程的Node節點。聯繫上面的代碼示例和源碼可以總結一下AQS和ConditionObject中的隊列是怎麼被維護的:

(1) 示例中存在兩個線程:main線程 t1線程

(2) 當main線程調用lock.locak()方法時,main獲取到鎖,繼續執行, 當執行到t1.start()方法時,t1也想獲取lock,但是此時該鎖已經被main線程佔用,所以t1線程進入 AQS維護的等待隊列中,等待機會獲取cpu的佔用權。

(3) 當main線程執行了condition.await()方法時,main線程就在釋放佔用鎖的同時加入到了ConditionObject維護的等待隊列中,在這個隊列中的線程,如果signal狀態不發生改變,是永遠沒有機會獲取到cpu的佔有權的。

(4) 好,這個時候main已經進入了ConditionObject維護的等待隊列中,那麼AQS維護的等待隊列中的t1線程就可以獲取cpu的佔有權了,可以繼續執行。

(5) 當t1線程執行到 condition.signal() 方法時,就會喚醒ConditionObject維護的等待隊列中的頭節點,也就是main線程。但是注意,這裏喚醒的意思是將main線程節點放到AQS維護的等待隊列中,然後聽從AQS的調度,並不是馬上就能獲取cpu的佔有權。

(6) 然後t1線程執行結束,unlock釋放佔用的鎖,在AQS維護的等待隊列中的main就能繼續執行下去了。

總之:

  • ConditionObject維護的等待隊列的功能是存放那些執行了await方法的線程,等待收到signal信息好可以進入AQS維護的等待隊列中。在這個隊列中是不會獲取到鎖的。
  • AQS維護的等待隊列存放那些就緒狀態的線程,只等待目前佔有鎖的傢伙執行完或者進入了ConditionObject維護的等待隊列中之後,來進行競爭獲取鎖。只有在這個隊列中才有資格(並不一定會)獲取鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章