網頁

2017/10/11

Java 執行緒 wait()

Java 多執行緒中,在目前的執行緒呼叫物件的wait()方法可以讓目前的執行緒暫停執行,等到其他的執行緒呼叫該物件的notify()notifyAll()方法後才會繼續執行。

wait()方法定義在Object類別,因此所有的物件都可呼叫此方法。

wait()要在同步方法(synchronized method)或同步區塊(synchronized block)中呼叫,即以關鍵字synchronized宣告的方法或區塊。

// synchronized block
synchronized(obj){
  //...
  obj.wait();
  //...
}

// synchronized method
public synchronized void method() { 
  // ...
}   

執行緒進入同步區塊後即擁有物件的監控(monitor),或稱該執行緒取得物件監控的鎖(lock)。若呼叫wait()時執行緒並沒有該物件的監控則會拋出IllegalMonitorStateException例外

執行緒呼叫wait()後會釋放物件的鎖並進入等待區(wait set),接著系統排程中的其他執行緒開始競爭物件的鎖。等待區的執行緒必須在其他執行緒呼叫物件的notify()notifyAll()方法,或經過指定的時間後才會重新進入排程。

等待區的執行緒在休眠狀態,不會進入排程,必須等到以下之一發生才會被喚醒。

  • 其他的執行緒呼叫該物件的notify()方法,且在等待區的執行緒剛好是被通知的對象。
  • 其他的執行緒呼叫該物件的notifyAll()方法。等待區中的所有執行緒皆會被通知並競爭物件的鎖。
  • 其他的執行緒中斷(interrupt)了在等待中的執行緒
  • 指定的等待時間已經結束。

等待區的執行緒被喚醒後,若取得物件的鎖便會重新進入排程,也就是回復可執行(runnable)狀態。當執行緒取回物件的鎖時,會從原來呼叫wait()的位置繼續執行。

當等待中的執行緒被中斷(interrupt)時,會發生InterruptedException例外。

當執行緒呼叫物件的wait(),時,執行緒只是釋放該物件的鎖並進入等待區,但等待中的執行緒對其他物件仍然可以存取。


範例一,宣告一個Message類別,其物件為執行緒要鎖定的對象,呼叫wait()notify()方法。

public class Message {
  
  private String msg;
  
  public Message(String str){
    this.msg = str;
  }

  public String getMsg() {
    return msg;
  }

  public void setMsg(String msg) {
    this.msg = msg;
  }
  
}

WaiterRunnable的實作,此執行緒在同步區塊中呼叫Message物件的wait()並進入等待區,直到被Notifier通知才會繼續執行。

public class Waiter implements Runnable{
  
  private Message message;
  
  public Waiter(Message message){
    this.message = message;
  }

  @Override
  public void run() {
    String threadName = Thread.currentThread().getName();
    synchronized(message){
      try{
        System.out.println(threadName +" 開始等待");
        message.wait();
      }catch(InterruptedException e){
        e.printStackTrace();
      }
      System.out.println(threadName + " 受到通知");
      System.out.println(threadName + " 處理訊息:" + message.getMsg());
    }
  }
}

NotifierRunnable的實作,此執行緒在同步區塊中呼叫Messagenotify()來通知等待區執行緒。

public class Notifier implements Runnable{
  
  private Message message;
  
  public Notifier(Message message){
    this.message = message;
  }

  @Override
  public void run() {
    String threadName = Thread.currentThread().getName();
    System.out.println(threadName + " 開始");
    
      try {
        System.out.println(threadName + " sleep一秒");
        Thread.sleep(1000);
        synchronized(message){
          message.setMsg("在notifier設定的新訊息");
          message.notify();
//          message.notifyAll();
        }
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    System.out.println(threadName + " 結束");
  }
}

測試

public class Test {

  public static void main(String[] args) {
    
    Message message = new Message("原本的訊息");
    Waiter waiter = new Waiter(message);
    new Thread(waiter,"waiter").start();
    
    Waiter waiter1 = new Waiter(message);
    new Thread(waiter1, "waiter1").start();
    
    Notifier notifier = new Notifier(message);
    new Thread(notifier, "notifier").start();
    System.out.println("全部的執行緒已經開始執行");
    
  }
}

執行結果如下

全部的執行緒已經開始執行
waiter1 開始等待
waiter 開始等待
notifier 開始
notifier sleep一秒
notifier 結束
waiter1 受到通知
waiter1 處理訊息:在notifier設定的新訊息


範例一呼叫notify()時程式並沒有結束,waiter仍在等待狀態,因為notify()只會通知任一等待區中的執行緒,所以只有waiter1收到通知並繼續執行,而waiter則沒有其他執行緒去喚醒它。如果改用notifyAll()則全部的執行續皆會被喚醒然後執行到結束。



範例二,執行緒Printer用來印出Counter的累加結果,進入同步區塊後先呼叫wait()來釋放counter的鎖並進入等待區。

public class Printer implements Runnable{
  
  private Counter counter;
  
  public Printer(Counter counter){
    this.counter = counter;
  }

  @Override
  public void run() {
    String threadName = Thread.currentThread().getName();
    System.out.println(threadName + " 開始");
    synchronized(counter){
      try {
        counter.wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println(threadName + " total:" + counter.getTotal());
    System.out.println(threadName + " 結束");
  }
}

Counter在同步區塊中累加完後會呼叫notify()來通知等待counter的鎖的執行緒。

public class Counter implements Runnable{
  
  private int total = 0;
  private int times;
  
  @Override
  public void run() {
    String threadName = Thread.currentThread().getName();
    System.out.println(threadName + " 開始");
    try {
      Thread.sleep(1000); // 確保Printer.run()中的counter.wait()先執行
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    synchronized(this){
      for(int i = 1; i <= times ; i++){
        total += i;
      }
      notify();
    }
    System.out.println(threadName + " 結束");
  }
  
  public void setCountTimes(int times){
    this.times = times;
  }
  
  public int getTotal(){
    return total;
  }
  
}

測試

public class Test {

  public static void main(String[] args) {
    
    Counter counter = new Counter();
    counter.setCountTimes(10);
    
    Thread p = new Thread(new Printer(counter), "printer");
    Thread c = new Thread(counter, "counter");
    
    p.start();
    c.start();
  }
}

執行結果

counter 開始
printer 開始
counter 結束
printer total:55
printer 結束

範例二的執行順序可以用下圖來表示



沒有留言:

張貼留言