1. 程式人生 > 程式設計 >Java 多執行緒基礎(二)

Java 多執行緒基礎(二)

簡介

在上篇 Java 多執行緒基礎(一) 我們提到了一些執行緒的常用方法,這篇我們具體看看其中一些方法的使用以及方法的區別,讓我們在工作中更好的使用。

wait 方法與 notify 方法

Object 類中定義了 wait 方法和 notify 方法,wait 方法的作用是讓當前執行緒進入等待狀態,將當前執行緒置入 預執行佇列,會在 wait 方法所在程式碼處停止執行,直到被通知或者被中斷,在呼叫 wait 方法之前,執行緒必須獲取該物件的鎖,因此只能在同步方法或者同步程式碼塊中呼叫 wait 方法,並且該方法會釋放當前執行緒鎖持有的鎖。notify 方法是喚醒在當前物件上等待的單個執行緒

,如果有多個執行緒等待,那麼執行緒排程器會挑出一個 wait 的執行緒,對其發出 notify ,並使它等待獲取該物件的物件鎖,這意味著,即使收到了通知,執行緒也不會立即獲取到物件鎖,必須等待 notify 方法的執行緒釋放鎖才可以。和 wait 方法一樣,notify 方法也只能在同步方法或者同步程式碼塊中呼叫。它還有個相似的方法 notifyAll,它的作用是喚醒在當前物件上等待的所有執行緒

下面通過一個生產者消費者來說明 wait 方法和 notify 方法的使用:

/**
 * @author mghio
 * @date: 2019-12-14
 * @version: 1.0
 * @description
: 執行緒 wait() 和 notify() 方法使用示例 * @since JDK 1.8 */
public class ThreadWaitAndNotifyDemo { public static void main(String[] args) { Producer producer = new Producer(); producer.start(); new Consumer("Consumer One",producer).start(); new Consumer("Consumer Two",producer).start(); new
Consumer("Consumer Three",producer).start(); new Consumer("Consumer Four",producer).start(); } static class Producer extends Thread { List<String> messageList = new ArrayList<>(2); @Override public void run() { try { while (true) { Thread.sleep(2000); synchronized (messageList) { String message = String.format("producer message [create time:%s]",LocalDateTime.now()); messageList.add(message); System.out.println("Producer " + getName() + " producer a msg: " + message); messageList.notify(); } } } catch (Exception e) { e.printStackTrace(); } } String getMessage() { synchronized (messageList) { if (messageList.size() == 0) { try { messageList.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } return messageList.remove(0); } } } static class Consumer extends Thread { private Producer producer; public Consumer(String name,Producer producer) { super(name); this.producer = producer; } @Override public void run() { while (true) { String message = producer.getMessage(); System.out.println("Consumer " + getName() + " get a msg: " + message); } } } } 複製程式碼

輸出結果如下:

Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:42.319]
Consumer Consumer One get a msg: producer message [create time:2019-12-14T22:45:42.319]
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:44.324]
Consumer Consumer Two get a msg: producer message [create time:2019-12-14T22:45:44.324]
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:46.325]
Consumer Consumer Three get a msg: producer message [create time:2019-12-14T22:45:46.325]
Producer Thread-0 producer a msg: producer message [create time:2019-12-14T22:45:48.328]
Consumer Consumer Four get a msg: producer message [create time:2019-12-14T22:45:48.328]
複製程式碼

消費者執行緒迴圈呼叫生產者的 getMessage 方法獲取訊息,如果訊息列表 messageList 為空,則呼叫訊息列表的 wait 方法讓執行緒進入等待狀態,生產者每隔 2 秒生成訊息並放入訊息列表 messageList 中,放入成功後呼叫 notify 方法喚醒一個處於 wait 狀態的執行緒去消費訊息,需要注意的是,在呼叫 waitnotify 方法時必須要先獲得該物件的鎖,上面的示例中是在 synchronized 程式碼塊中呼叫的。

sleep 方法

waitnotify 方法不同,sleep 方法定義在 Thread 類中,從方法名也可以知道,這個方法的作用就是讓當前執行緒休眠,即呼叫該方法後當前執行緒會從執行狀態(Running)狀態進入到阻塞(休眠)狀態(Blocked),同時該方法必須指定休眠的時間,當前執行緒的休眠時間會大於或者等於這個指定的休眠時間。當執行緒重新被喚醒時,執行緒會由阻塞狀態(Blocked)變成就緒狀態(Runnable),然後等待 CPU 的排程執行。sleep 方法的示例程式碼如下:

/**
 * @author mghio
 * @date: 2019-12-14
 * @version: 1.0
 * @description: 執行緒 sleep() 方法使用示例
 * @since JDK 1.8
 */
public class ThreadSleepDemo {

  private static Object object = new Object();

  public static void main(String[] args) {
    MyThread myThreadOne = new MyThread("t1");
    MyThread myThreadTwo = new MyThread("t2");
    myThreadOne.start();
    myThreadTwo.start();
  }

  static class MyThread extends Thread {

    public MyThread(String name) {
      super(name);
    }

    @Override
    public void run() {
      synchronized (object) {
        try {
          for (int i = 0; i < 5; i++) {
            System.out.println(String.format("%s: %d",this.getName(),i));
            if (i % 2 == 0) {
              Thread.sleep(2000);
            }
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
  }
}
複製程式碼

輸出結果如下:

t1: 0
t1: 1
t1: 2
t1: 3
t1: 4
t2: 0
t2: 1
t2: 2
t2: 3
t2: 4
複製程式碼

我們啟動了兩個執行緒 t1t2,兩個執行緒的 run 方法引用了同一個物件 object 的同步鎖(synchronized (object)),雖然在第一個執行緒 t1 中當 i 被 2 整除時會呼叫 Thread.sleep(2000) 讓當前執行緒休眠 2 s,但是此時執行緒 t2 也不會得到 cpu 的執行權去執行,因為 t1 執行緒呼叫 sleep 方法並沒有釋放object所持有的同步鎖。如果我們註釋掉 synchronized (object) 後再次執行該程式,執行緒 t1t2 是可以交替執行的,註釋之後的輸出結果如下:

t2: 0
t1: 0
t1: 1
t2: 1
t1: 2
t2: 2
t2: 3
t1: 3
t2: 4
t1: 4
複製程式碼

yield 方法

yield 方法定義在 Thread 類中,是執行緒特有的方法。此方法的主要作用是讓步,它會使當前執行緒從執行狀態(Running)變為就緒狀態(Runnable),從而讓其他具有同樣優先順序的處於就緒狀態的執行緒獲取到 CPU 執行權(PS: CPU 會從眾多的處於就緒狀態的執行緒裡選擇,也就是說,當前也就是剛剛的那個執行緒還是有可能會被再次執行到的,並不是說一定會執行其他執行緒而該執行緒在下一次中不會執行到),但是,也並不能保證在當前執行緒呼叫 yield 之後,其它哪些具有相同優先順序的執行緒就一定能獲得執行權,也有可能是當前執行緒又進入到執行狀態(Running)繼續執行。yield 方法的示例程式碼如下:

/**
 * @author mghio
 * @date: 2019-12-14
 * @version: 1.0
 * @description: 執行緒 yield() 方法使用示例
 * @since JDK 1.8
 */
public class ThreadYieldDemo {

  public static void main(String[] args) {
    MyThread myThreadOne = new MyThread("t1");
    MyThread myThreadTwo = new MyThread("t2");
    myThreadOne.start();
    myThreadTwo.start();
  }

  static class MyThread extends Thread {

    MyThread(String name) {
      super(name);
    }

    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
        System.out.println(String.format("%s [%d] ---> %d",this.getPriority(),i));
        if (i % 2 == 0) {
          yield();
        }
      }
    }
  }
}
複製程式碼

輸出結果如下:

t1 [5] ---> 0
t2 [5] ---> 0
t1 [5] ---> 1
t1 [5] ---> 2
t1 [5] ---> 3
t1 [5] ---> 4
t1 [5] ---> 5
t1 [5] ---> 6
t1 [5] ---> 7
t1 [5] ---> 8
t1 [5] ---> 9
t2 [5] ---> 1
t2 [5] ---> 2
t2 [5] ---> 3
t2 [5] ---> 4
t2 [5] ---> 5
t2 [5] ---> 6
t2 [5] ---> 7
t2 [5] ---> 8
t2 [5] ---> 9
複製程式碼

從以上輸出結果可以看出,執行緒 t1 中的變數 i 在被 2 整除的時候,並沒有切換到執行緒 t2 去執行,這也驗證了我們上文說的,yield 方法雖然可以讓執行緒由執行狀態變成就緒狀態,但是,它不一定會讓其它執行緒獲取 CPU 執行權從而進入到執行狀態,即使這個其它執行緒和當前具有相同的優先順序,yield 方法不會釋放鎖(證明方法只需將上面這個示例的 run 方法裡面加上 synchronized (obj) 即可,此時 t2 執行緒會等到執行緒 t1 執行完畢後才會執行)。

join 方法

在有些場景中我們需要在子執行緒去執行一些耗時的任務,但是我們的主執行緒又必須等待子執行緒執行完畢之後才能結束,那麼此時就可以使用 join 方法了,該方法定義在 Thread 類中,方法的作用是:讓主執行緒等待子執行緒執行結束之後才能繼續執行,下面我們通過一個例子來看看:

/**
 * @author mghio
 * @date: 2019-12-15
 * @version: 1.0
 * @description: 執行緒 join() 方法使用示例
 * @since JDK 1.8
 */
public class ThreadJoinDemo {

  public static void main(String[] args) {
    try {
      MyThread myThread = new MyThread("t1");
      myThread.start();
      myThread.join();
      System.out.println(String.format("%s ---> %s finish",LocalDateTime.now(),Thread.currentThread().getName()));
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  static class MyThread extends Thread {

    MyThread(String name) {
      super(name);
    }

    @Override
    public void run() {
      System.out.println(String.format("%s ---> %s start",this.getName()));
      // 模擬耗時操作
      try {
        Thread.sleep(2000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(String.format("%s ---> %s finish",this.getName()));
    }
  }
}
複製程式碼

輸出結果如下:

2019-12-15T00:22:55.971 ---> t1 start
2019-12-15T00:22:57.984 ---> t1 finish
2019-12-15T00:22:57.985 ---> main finish
複製程式碼

在主執行緒 main 中通過 new MyThread("t1") 新建執行緒 t1。 接著,通過 t1.start() 啟動執行緒 t1,在執行 t1.join()之後, 主執行緒會進入阻塞狀態等待 t1 執行結束。子執行緒 t1 結束之後,會喚醒主執行緒,主執行緒重新獲取 CPU 執行權,主執行緒繼續往下執行。在使用了 join 方法之後主執行緒會等待子執行緒結束之後才會結束。

總結

以上是執行緒一些常用的方法介紹和具體使用知識總結。