1. 程式人生 > >java wait與notify

java wait與notify

前言

我們知道,java的wait/notify的通知機制可以用來實現執行緒間通訊。wait表示執行緒的等待,呼叫該方法會導致執行緒阻塞,直至另一執行緒呼叫notify或notifyAll方法才可另其繼續執行。經典的生產者、消費者模式即是使用wait/notify機制得以完成。在這篇文章中,我們將深入解析這一機制,瞭解其背後的原理。

執行緒的狀態

在瞭解wait/notify機制前,先熟悉一下java執行緒的幾個生命週期。分別為初始(NEW)、執行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超時等待(TIMED_WAITING)、終止(TERMINATED)等狀態(位於java.lang.Thread.State列舉類中)。

以下是對這幾個狀態的簡要說明,詳細說明見該類註釋。

狀態名稱 說明
NEW 初始狀態,執行緒被構建,但未呼叫start()方法
RUNNABLE 執行狀態,呼叫start()方法後。在java執行緒中,將作業系統執行緒的就緒和執行統稱執行狀態
BLOCKED   阻塞狀態,執行緒等待進入synchronized程式碼塊或方法中,等待獲取鎖
WAITING  等待狀態,執行緒可呼叫wait、join等操作使自己陷入等待狀態,並等待其他執行緒做出特定操作(如notify或中斷)
TIMED_WAITING  超時等待,執行緒呼叫sleep(timeout)、wait(timeout)等操作進入超時等待狀態,超時後自行返回
TERMINATED  終止狀態,執行緒執行結束

對於以上執行緒間的狀態及轉化關係,我們需要知道

WAITING(等待狀態)和TIMED_WAITING(超時等待)都會令執行緒進入等待狀態,不同的是TIMED_WAITING會在超時後自行返回,而WAITING則需要等待至條件改變。

進入阻塞狀態的唯一前提是在等待獲取同步鎖。java註釋說的很明白,只有兩種情況可以使執行緒進入阻塞狀態:一是等待進入synchronized塊或方法,另一個是在呼叫wait()方法後重新進入synchronized塊或方法。下文會有詳細解釋。

Lock類對於鎖的實現不會令執行緒進入阻塞狀態,Lock底層呼叫LockSupport.park()方法,使執行緒進入的是等待狀態。

wait/notify用例

讓我們先通過一個示例解析

synchronized,的主要機制使得鎖定到當前執行緒,其他執行緒需等待此執行緒釋放鎖,才得以訪問,而wait()方法可以使執行緒進入等待狀態,(暫時放棄此物件的鎖,使自己陷入等待佇列中),而notify()可以使等待的狀態喚醒(使得上一個利用wait陷入等待的執行緒繼續執行)。這樣的同步機制十分適合生產者、消費者模式:消費者消費某個資源,而生產者生產該資源。當該資源缺失時,消費者呼叫wait()方法進行自我阻塞,等待生產者的生產;生產者生產完畢後呼叫notify/notifyAll()喚醒消費者進行消費。

以下是程式碼示例,其中flag標誌表示資源的有無。

public class ThreadTest {

    static final Object obj = new Object();

    private static boolean flag = false;

    public static void main(String[] args) throws Exception {

        Thread consume = new Thread(new Consume(), "Consume");
        Thread produce = new Thread(new Produce(), "Produce");
        consume.start();
        Thread.sleep(1000);
        produce.start();

        try {
            produce.join();
            consume.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 生產者執行緒
    static class Produce implements Runnable {

        @Override
        public void run() {

            synchronized (obj) {
                System.out.println("進入生產者執行緒");
                System.out.println("生產");
                try {
                    TimeUnit.MILLISECONDS.sleep(2000);  //模擬生產過程
                    flag = true;
                    obj.notify();  //通知消費者
                    TimeUnit.MILLISECONDS.sleep(1000);  //模擬其他耗時操作
                    System.out.println("退出生產者執行緒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //消費者執行緒
    static class Consume implements Runnable {

        @Override
        public void run() {
            synchronized (obj) {
                System.out.println("進入消費者執行緒");
                System.out.println("wait flag 1:" + flag);
                while (!flag) {  //判斷條件是否滿足,若不滿足則等待
                    try {
                        System.out.println("還沒生產,進入等待");
                        obj.wait();
                        System.out.println("結束等待");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("wait flag 2:" + flag);
                System.out.println("消費");
                System.out.println("退出消費者執行緒");
            }

        }
    }
}

輸出結果為:

進入消費者執行緒 

wait flag 1:false 

還沒生產,進入等待 

進入生產者執行緒 

生產 

退出生產者執行緒 

結束等待 

wait flag 2:true 

消費 

退出消費者執行緒 

 

理解了輸出結果的順序,也就明白了wait/notify的基本用法。有以下幾點需要知道:

在示例中沒有體現但很重要的是,wait/notify方法的呼叫必須處在該物件的鎖(Monitor)中,也即,在呼叫這些方法時首先需要獲得該物件的鎖。否則會爬出IllegalMonitorStateException異常。
從輸出結果來看,在生產者呼叫notify()後,消費者並沒有立即被喚醒,而是等到生產者退出同步塊後才喚醒執行。(這點其實也好理解,synchronized同步方法(塊)同一時刻只允許一個執行緒在裡面,生產者不退出,消費者也進不去)
注意,消費者被喚醒後是從wait()方法(被阻塞的地方)後面執行,而不是重新從同步塊開頭。

深入瞭解

這一節我們探討wait/notify與執行緒狀態之間的關係。深入瞭解執行緒的生命週期。

由前面執行緒的狀態轉化圖可知,當呼叫wait()方法後,執行緒會進入WAITING(等待狀態),後續被notify()後,並沒有立即被執行,而是進入等待獲取鎖的阻塞佇列。

對於每個物件來說,都有自己的等待佇列和阻塞佇列。以前面的生產者、消費者為例,我們拿obj物件作為物件鎖,配合圖示。內部流程如下

當執行緒A(消費者)呼叫wait()方法後,執行緒A讓出鎖,自己進入等待狀態,同時加入鎖物件的等待佇列。
執行緒B(生產者)獲取鎖後,呼叫notify方法通知鎖物件的等待佇列,使得執行緒A從等待佇列進入阻塞佇列。
執行緒A進入阻塞佇列後,直至執行緒B釋放鎖後,執行緒A競爭得到鎖繼續從wait()方法後執行。