天天看點

Notify通知機制解析Wait/Notify通知機制解析

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 終止狀态,線程運作結束
Notify通知機制解析Wait/Notify通知機制解析

對于以上線程間的狀态及轉化關系,我們需要知道

  1. WAITING(等待狀态)和TIMED_WAITING(逾時等待)都會令線程進入等待狀态,不同的是TIMED_WAITING會在逾時後自行傳回,而WAITING則需要等待至條件改變。
  2. 進入阻塞狀态的唯一前提是在等待擷取同步鎖。java注釋說的很明白,隻有兩種情況可以使線程進入阻塞狀态:一是等待進入synchronized塊或方法,另一個是在調用wait()方法後重新進入synchronized塊或方法。下文會有詳細解釋。
  3. Lock類對于鎖的實作不會令線程進入阻塞狀态,Lock底層調用LockSupport.park()方法,使線程進入的是等待狀态。

wait/notify用例

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

wait()方法可以使線程進入等待狀态,而notify()可以使等待的狀态喚醒。這樣的同步機制十分适合生産者、消費者模式:消費者消費某個資源,而生産者生産該資源。當該資源缺失時,消費者調用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的基本用法。有以下幾點需要知道:

  1. 在示例中沒有展現但很重要的是,**wait/notify方法的調用必須處在該對象的鎖(Monitor)中,也即,在調用這些方法時首先需要獲得該對象的鎖。**否則會抛出IllegalMonitorStateException異常。
  2. 從輸出結果來看,在生産者調用notify()後,消費者并沒有立即被喚醒,而是等到生産者退出同步塊後才喚醒執行。(這點其實也好了解,synchronized同步方法(塊)同一時刻隻允許一個線程在裡面,生産者不退出,消費者也進不去)
  3. 注意,消費者被喚醒後是從wait()方法(被阻塞的地方)後面執行,而不是重新從同步塊開始。

深入了解

這一節我們探讨wait/notify與線程狀态之間的關系。深入了解線程的生命周期。

由前面線程的狀态轉化圖可知,當調用wait()方法後,線程會進入WAITING(等待狀态),後續被notify()後,并沒有立即被執行,而是進入等待擷取鎖的阻塞隊列。

Notify通知機制解析Wait/Notify通知機制解析

對于每個對象來說,都有自己的等待隊列和阻塞隊列。以前面的生産者、消費者為例,我們拿obj對象作為對象鎖,配合圖示。内部流程如下

  1. 當線程A(消費者)調用wait()方法後,線程A讓出鎖,自己進入等待狀态,同時加入鎖對象的等待隊列。
  2. 線程B(生産者)擷取鎖後,調用notify方法通知鎖對象的等待隊列,使得線程A從等待隊列進入阻塞隊列。
  3. 線程A進入阻塞隊列後,直至線程B釋放鎖後,線程A競争得到鎖繼續從wait()方法後執行。

繼續閱讀