說說Java執行緒間通訊

城南書客發表於2019-07-28

 序言

 

 

 

 

 正文

[一] Java執行緒間如何通訊?


執行緒間通訊的目標是使執行緒間能夠互相傳送訊號,包括如下幾種方式:

1、通過共享物件通訊

執行緒間傳送訊號的一個簡單方式是在共享物件的變數裡設定訊號值;執行緒A在一個同步塊裡設定boolean型成員變數hasDataToProcess為true,執行緒B也在同步塊裡讀取hasDataToProcess這個成員變數;執行緒A和B必須獲得指向一個MySignal共享例項的引用,以便進行通訊;如果它們持有的引用指向不同的MySingal例項,那麼彼此將不能檢測到對方的訊號;需要處理的資料可以存放在一個共享快取區裡,它和MySignal例項是分開存放的。示例如下:

public class MySignal{
  protected boolean hasDataToProcess = false;

  public synchronized boolean getHasDataToProcess(){
    return this.hasDataToProcess;
  }
  public synchronized void setHasDataToProcess(boolean hasData){
    this.hasDataToProcess = hasData;
  }
}
View Code

【場景展現】:

B同學去了圖書館,發現這本書被借走了(執行了例子中的hasDataToProcess),他回到宿舍,等了幾天,再去圖書館找這本書,發現這本書已經被還回,他順利借走了書。

2、忙等待

準備處理資料的執行緒B正在等待資料變為可用;換句話說,它在等待執行緒A的一個訊號,這個訊號使hasDataToProcess()返回true,執行緒B執行在一個迴圈裡,以等待這個訊號。示例如下:

protected MySignal sharedSignal = ...
...
while(!sharedSignal.hasDataToProcess()){
  //do nothing... busy waiting
}
View Code

【場景展現】:

假如A同學在B同學走後一會就把書還回去了,B同學卻是在幾天後再次去圖書館找的書,為了早點借到書(減少延遲),B同學可以就在圖書館等著,比如,每隔幾分鐘(while迴圈)他就去檢查這本書有沒有被還回,這樣只要A同學一還回書,B同學很快就會知道。

3、wait(),notify()和notifyAll()

忙等待沒有對執行等待執行緒的CPU進行有效的利用,除非平均等待時間非常短,否則,讓等待執行緒進入睡眠或者非執行狀態更為明智,直到它接收到它等待的訊號。

一個執行緒一旦呼叫了任意物件的wait()方法,就會變為非執行狀態,直到另一個執行緒呼叫了同一個物件的notify()方法;為了呼叫wait()或者notify(),執行緒必須先獲得那個物件的鎖;也就是說,執行緒必須在同步塊裡呼叫wait()或者notify()。示例如下:

public class MonitorObject{
}

public class MyWaitNotify{
  MonitorObject myMonitorObject = new MonitorObject();

  public void doWait(){
    synchronized(myMonitorObject){
      try{
        myMonitorObject.wait();
      } catch(InterruptedException e){...}
    }
  }
  public void doNotify(){
    synchronized(myMonitorObject){
      myMonitorObject.notify();
    }
  }
}
View Code

等待執行緒呼叫doWait(),而喚醒執行緒呼叫doNotify();當一個執行緒呼叫一個物件的notify()方法,正在等待該物件的所有執行緒中將有一個執行緒被喚醒並允許執行(這個將被喚醒的執行緒是隨機的,不可以指定喚醒哪個執行緒),可以使用notifyAll()方法來喚醒正在等待一個指定物件的所有執行緒。

【場景展現】:

檢查很多次後,B同學發現這樣做自己太累了,身體有點吃不消,不過很快,學校圖書館系統改進,加入了簡訊通知功能(notify()),只要A同學一還回書,立馬會簡訊通知B同學,這樣B同學就可以在家睡覺等簡訊了。

4、丟失的訊號

notify()和notifyAll()方法不會儲存呼叫它們的方法,因為當這兩個方法被呼叫時,有可能沒有執行緒處於等待狀態,通知訊號過後便丟棄了;因此,如果一個執行緒先於被通知執行緒呼叫wait()前呼叫了notify(),等待的執行緒將錯過這個訊號,在某些情況下,這可能使等待執行緒永遠在等待,不再醒來,因為執行緒錯過了喚醒訊號。
為了避免丟失訊號,必須把它們儲存在訊號類裡。示例如下:

public class MyWaitNotify2{
  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      if(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}
View Code

【場景展現】:

學校圖書館系統是這麼設計的:當一本書被還回來的時候,會給等待者傳送簡訊,並且只會發一次,如果沒有等待者,他也會發(只不過沒有接收者),這樣問題就出現了,因為簡訊只會發一次,當書被還回來的時候,沒有人等待借書,他會發一條空簡訊,但是之後有等待藉此本書的同學永遠也不會再收到簡訊,導致這些同學會無休止的等待;為了避免這個問題,我們在等待的時候先打個電話問問圖書館管理員是否繼續等待(if(!wasSignalled))。

5、假喚醒

由於某種原因,執行緒有可能在沒有呼叫過notify()和notifyAll()的情況下醒來,這就是所謂的假喚醒(spurious wakeups)。

如果在MyWaitNotify2的doWait()方法裡發生了假喚醒,等待執行緒即使沒有收到正確的訊號,也能夠執行後續的操作,這可能出現嚴重問題。

為了防止假喚醒,儲存訊號的成員變數將在一個while迴圈裡接受檢查,而不是在if表示式裡,這樣的一個while迴圈叫做自旋鎖(這種做法會消耗CPU,如果長時間不呼叫doNotify方法,doWait方法會一直自旋,CPU會有很大消耗),被喚醒的執行緒會自旋直到自旋鎖(while迴圈)裡的條件變為false。示例如下:

public class MyWaitNotify3{
  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }
  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}
View Code

 【場景展現】:

圖書館系統還有一個bug:系統會偶爾給你發條錯誤簡訊,說書可以借了(其實書不可以借),我們之前已經給圖書館管理員打過電話了,他說讓我們等簡訊,我們很聽話,一等到簡訊(其實是bug引起的錯誤簡訊),就去借書了,到了圖書館後發現這書根本就沒還回來!我們很鬱悶,但也沒辦法啊,學校不修復bug,我們得聰明點:每次在收到簡訊後,再打電話問問書到底能不能借(while(!wasSignalled))。

 

[二] 多個執行緒如何按順序執行?


多個執行緒如何保證執行順序,是一個很高頻的面試題,實現方式很多,這裡介紹四種實現方式:

1、使用Thread的join方法

Thread類中的join方法的主要作用就是同步,呼叫執行緒需等待join執行緒執行完或指定時間後執行,如:join(10),表示等待某執行緒執行10秒後再執行。示例如下:

public class ThreadChildJoin {
    public static void main(String[] args) {
        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("需求分析...");
            }
        });

        final Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    t1.join();
                    System.out.println("功能開發...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    t2.join();
                    System.out.println("功能測試...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t3.start();
        t1.start();
        t2.start();
    }
}
View Code

2、使用Condition(條件變數)

Condition是一個多執行緒間協調通訊的工具類,使得某個或者某些執行緒一起等待某個條件(Condition),只有當該條件具備( signal 或者 signalAll方法被帶呼叫)時 ,這些等待執行緒才會被喚醒,從而重新爭奪鎖。

Condition類主要方法包括:await方法(類似於Object類中的wait()方法)、signal方法(類似於Object類中的notify()方法)、signalAll方法(類似於Object類中的notifyAll()方法)。示例如下:

public class ThreadCondition {
    private static Lock lock = new ReentrantLock();
    private static Condition condition1 = lock.newCondition();
    private static Condition condition2 = lock.newCondition();

    /**
     * 為什麼要加這兩個標識狀態?
     * 如果沒有狀態標識,當t1已經執行完了t2才執行,t2在等待t1喚醒導致t2永遠處於等待狀態
     */
    private static Boolean t1Run = false;
    private static Boolean t2Run = false;

    public static void main(String[] args) {

        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                System.out.println("需求分析...");
                t1Run = true;
                condition1.signal();
                lock.unlock();
            }
        });

        final Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    if(!t1Run){
                        condition1.await();
                    }
                    System.out.println("功能開發...");
                    t2Run = true;
                    condition2.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock.unlock();
            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    if(!t2Run){
                        condition2.await();
                    }
                    System.out.println("功能測試...");
                    lock.unlock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t3.start();
        t1.start();
        t2.start();
    }
}
View Code

3、使用CountDownLatch(倒計數)

顧名思義,使用CountDownLatch可以實現類似計數器的功能。示例如下:

public class ThreadCountDownLatch {
    private static CountDownLatch c1 = new CountDownLatch(1);

    /**
     * 用於判斷執行緒二是否執行,倒數計時設定為1,執行後減1
     */
    private static CountDownLatch c2 = new CountDownLatch(1);

    public static void main(String[] args) {
        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("需求分析...");
                //對c1倒數計時-1
                c1.countDown();
            }
        });

        final Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //等待c1倒數計時,計時為0則往下執行
                    c1.await();
                    System.out.println("功能開發...");
                    //對c2倒數計時-1
                    c2.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //等待c2倒數計時,計時為0則往下執行
                    c2.await();
                    System.out.println("功能測試...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t3.start();
        t1.start();
        t2.start();
    }
}
View Code

4、使用CyclicBarrier(迴環柵欄)

CyclicBarrier可以實現讓一組執行緒等待至某個狀態之後再全部同時執行,“迴環”是因為當所有等待執行緒都被釋放以後,CyclicBarrier可以被重用,可以把這個狀態當做barrier,當呼叫await()方法之後,執行緒就處於barrier了。示例如下:

public class ThreadCyclicBarrier {
    static CyclicBarrier barrier1 = new CyclicBarrier(2);
    static CyclicBarrier barrier2 = new CyclicBarrier(2);

    public static void main(String[] args) {

        final Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("需求分析...");
                    //放開柵欄1
                    barrier1.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });

        final Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //放開柵欄1
                    barrier1.await();
                    System.out.println("功能開發...");
                    //放開柵欄2
                    barrier2.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });

        final Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //放開柵欄2
                    barrier2.await();
                    System.out.println("功能測試...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });

        t3.start();
        t1.start();
        t2.start();
    }
}
View Code

 

參考:

[1] http://ifeve.com/thread-signaling/

相關文章