1. 程式人生 > 實用技巧 >執行緒基本通訊機制--wait和notify

執行緒基本通訊機制--wait和notify

一、執行緒基本通訊機制

1 wait和notify的用法

wait和notify是Java最基本的執行緒間通訊機制,體現了執行緒的互動,用於資訊的傳遞。例如在生產者消費者模式中,利用阻塞和喚醒使不同執行緒之間配合實現業務邏輯。

阻塞階段--wait,呼叫物件的wait方法,執行緒進入WAITING狀態,阻塞掛起,釋放鎖
wait阻塞後,直到下面情況之一發生時,執行緒才會被喚醒。

  • 其他執行緒呼叫該物件的notify/notifyAll方法。
  • 帶有超時引數的wait方法,發生超時。如果引數是0,則永久等待。
  • 呼叫執行緒中斷interrupt()。執行緒在waiting狀態,會自動響應中斷,丟擲中斷異常。

喚醒階段 --notify/notifyAll

  • notify:隨機喚醒一個執行緒
  • notifyAll:喚醒所有執行緒

2 wait和notify性質

  1. 使用前需要擁有鎖(monitor鎖)
  2. 屬於Object類,底層是final native方法,屬於JVM層程式碼。
  3. wait和notify是最基本的執行緒通訊機制
  4. 同時擁有多把鎖,需要注意鎖的釋放順序
synchronized(this) { 
    while(條件){
       wait();     
    }
}
  • 如果wait方法沒有修飾,表示當前物件this
  • 即使沒有呼叫喚醒方法,執行緒仍有可能從掛起狀態變為可執行狀態(虛假喚醒)。為防患於未然,常見是不斷測試該執行緒被喚醒的條件是否被滿足,不滿足則繼續等待。
synchronized (resourceA) {
    synchronized (resourceB) {
        try {
            resourceA.wait(); // 只釋放resourceA鎖
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

注意點:呼叫某個物件的notify,只能喚醒與該物件對應的執行緒。呼叫wait也只釋放當前的那把鎖。

3 wait原理

EntrySet:入口集;WaitSet:等待集;The owner:執行緒擁有鎖。’
鎖的執行原理:開始執行緒在入口集和等待集競爭鎖【1】,此時執行緒A獲取到了鎖【2】,入口集和等待集中的執行緒進入BLOCKED。此時A可以正常執行完釋放鎖【6】,也可以呼叫wait釋放鎖進入等待集【3】。等待集執行緒被喚醒【4】後,進入另一個等待集,與入口集的執行緒一起競爭鎖【5】。

二、常見問題

1 wait和notify實現生產者消費者模式

生產者消費者模式可以解耦生產者和消費者,使兩者更好地配合。

// 生產和消費100個產品
public class ProducerConsumerModelByWaitAndNotify {
    public static void main(String[] args) {
        // 建立倉庫
        Storage storage = new Storage();
        // 建立生產者消費者執行緒
        Thread producer = new Thread(new ProducerTask(storage));
        Thread consumer = new Thread(new ConsumerTask(storage));
        producer.start();
        consumer.start();
    }
}

class ProducerTask implements Runnable {
    private Storage storage;

    public ProducerTask(Storage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        // 生產100個產品
        for (int i = 0; i < 100; i++) {
            storage.put();
        }
    }
}

class ConsumerTask implements Runnable {
    private Storage storage;

    public ConsumerTask(Storage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.take();
        }
    }
}

class Storage {
    private int maxSize;
    private Queue<Date> storage;

    public Storage() {
        this.maxSize = 10; // 佇列最大是10
        this.storage = new LinkedList<>();
    }

    /**
     * wait和notify需要首先獲取到鎖,因此需要使用synchronized方法或者同步程式碼塊
     */
    public synchronized void put() {
        // 倉庫已滿,無法生產更多產品,讓出鎖
        while (storage.size() == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(new Date());
        System.out.println("生產者生產產品,此時倉庫產品數:" + storage.size());
        // 通知消費者消費
        notify();
    }

    public synchronized void take() {
        // 倉庫為空,無法獲取到產品,執行緒讓出鎖
        while (storage.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.poll();
        System.out.println("消費者消費產品,此時倉庫產品數:" + storage.size());
        // 通知生產者生產
        notify();
    }
}

2. 為什麼wait需要放在同步程式碼塊中使用,而sleep不需要?

主要是為了讓通訊更加可靠,防止死鎖、永久等待的發生。

wait放到synchronized程式碼中對執行緒有一定的保護作用。假設沒有synchronized的保護,執行緒A在執行到wait語句之前,切換到執行緒B執行了notify語句,此時執行了wait語句釋放鎖後,沒有執行緒喚醒,導致了永久等待。

sleep方法是針對單個執行緒的,與其他執行緒無關,無需放入到同步程式碼塊中。

3 為什麼wait和notify方法定義在Object中,而不是Thread中?

wait和notify是鎖級別操作,而鎖是屬於某個物件的,鎖標識在物件的物件頭中。如果將wait和notify定義線上程中,則會有很大的侷限性。例如每個執行緒都可能會休眠。如果某個執行緒持有多個鎖,而且鎖之間是相互配合的時,wait方法在Thread類中,就沒有辦法實現執行緒的配合。

呼叫執行緒物件.wait會發生什麼?

個人理解:
呼叫執行緒物件的wait方法,也就是說以執行緒為鎖。wait和notify的初衷就是用來執行緒間通訊,如果以執行緒為鎖,不利於設計流程。