執行緒基本通訊機制--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性質
- 使用前需要擁有鎖(monitor鎖)
- 屬於Object類,底層是final native方法,屬於JVM層程式碼。
- wait和notify是最基本的執行緒通訊機制
- 同時擁有多把鎖,需要注意鎖的釋放順序
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的初衷就是用來執行緒間通訊,如果以執行緒為鎖,不利於設計流程。