juc包:使用 juc 包下的顯式 Lock 實現執行緒間通訊

Life_Goes_On發表於2020-10-09

一、前置知識


執行緒間通訊三要素:

多執行緒+判斷+操作+通知+資源類。

上面的五個要素,其他三個要素就是普通的多執行緒程式問題,那麼通訊就需要執行緒間的互相通知,往往伴隨著何時通訊的判斷邏輯。

在 java 的 Object 類裡就提供了對應的方法來進行通知,同樣的,保證安全的判斷採用隱式的物件鎖,也就是 synchronized 關鍵字實現。這塊內容在:

java多執行緒:執行緒間通訊——生產者消費者模型

已經寫過。


二、使用 Lock 實現執行緒間通訊


那麼,我們知道 juc 包裡提供了顯式的鎖,即 Lock 介面的各種實現類,如果想用顯式的鎖來實現執行緒間通訊問題,喚醒方法就要使用對應的 Conditon 類的 await 和 signalAll 方法。(這兩個方法名字也能看得出來,對應 Object 類的 wait 和 notifyAll)

Condition 物件的獲取可以通過具體的 Lock 實現類的物件的 newCondition 方法獲得。

public class Communication2 {
    public static void main(String[] args) {
        Container container = new Container();
        new Thread(()->{
            for (int i = 0; i < 10; i++){
                container.increment();
            }
        },"生產者1").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++){
                container.decrenment();
            }
        },"消費者1").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++){
                container.increment();
            }
        },"生產者2").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++){
                container.decrenment();
            }
        },"消費者2").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++){
                container.increment();
            }
        },"生產者3").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++){
                container.decrenment();
            }
        },"消費者3").start();
    }

}

class Container{
    private int count = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment(){
        lock.lock();
        try {
            while (count != 0){
                condition.await();
            }
            count++;
            System.out.println(Thread.currentThread().getName() + " "+count);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrenment(){
        lock.lock();
        try{
            while (count == 0){
                condition.await();
            }
            count--;
            System.out.println(Thread.currentThread().getName()+ " " + count);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
} 

輸出也沒有任何問題。

這裡面我們模擬了 3 個生產者和 3 個消費者,進行對一個資源類,其實就是一個數字 count 的操作,並使用 Condition 類來進行喚醒操作。

從目前程式碼的用法來看, juc 包的 Lock 介面實現類和之前使用 synchronized + Object 類的執行緒通訊方法是一樣的,但是併發包的開發工具給了他更多的靈活性。靈活在哪?


三、喚醒特定執行緒


新技術解決了舊問題,這個方法解決的問題就是,在生產者消費者問題裡:有時候我們並不想喚醒所有的對面夥伴,而只想要喚醒特定的一部分,這時候該怎麼辦呢?

如果沒有顯式的 lock,我們的思路可能是:

  1. 採用一個標誌物件,可以是一個數值或者別的;
  2. 當通訊呼叫 signalAll 的時候,其他執行緒都去判斷這個標誌,從而決定自己應不應該工作。

這種實現是可行的,但是本質上其他執行緒都被喚醒,然後一直阻塞+判斷,其實還是在競爭。那麼 Condition 類其實就已經提供了對應的方法,來完成這樣的操作:

我們看這樣一個需求:

  • 同樣是多執行緒操作、需要通訊。但是我們要指定各個執行緒交替的順序,以及指定喚醒的時候是指定哪個具體執行緒,這樣就不會存在喚醒所有執行緒然後他們之間互相競爭了。
  • 具體說是:AA 列印 5 次,BB 列印 10 次, CC 列印 15次,然後接著從 AA 開始一輪三個交替

程式碼如下:

public class TakeTurnPrint {
    public static void main(String[] args) {
        ShareResource resource = new ShareResource();
        new Thread(()->{resource.printA();},"A").start();
        new Thread(()->{resource.printB();},"B").start();
        new Thread(()->{resource.printC();},"C").start();
    }
}

/**
* 資源
*/
class ShareResource{
    private int signal = 0;//0-A,1-B,2-C
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();

    public void printA(){
        lock.lock();
        try{
            while (signal != 0){
                condition.await();//精準
            }
            for (int i = 0; i < 5; i++){
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
            signal = 1;//精準
            condition1.signal();//精準指定下一個
        }catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    
    public void printB(){
        lock.lock();
        try{
            while (signal != 1){
                condition1.await();//精準
            }
            for (int i = 0; i < 10; i++){
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
            signal = 2;//精準
            condition2.signal();//精準指定下一個
        }catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    
    public void printC(){
        lock.lock();
        try{
            while (signal != 2){
                condition2.await();//精準
            }
            for (int i = 0; i < 15; i++){
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
            signal = 0;//精準
            condition.signal();//精準指定下一個
        }catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

其中,使用三個 Condition 物件,用一個 signal 的不同值,來通知不同的執行緒。

相關文章