Java併發編程入門(七)輕鬆理解wait和notify以及使用場景

Java極客  |  做者  /  鏗然一葉
這是Java極客的第 35 篇原創文章

1、使用場景

wait常常被用於生產者和消費者模式,如圖:java


1.Producer負責生成任務並添加到TaskQueue中
2.Consumer從TaskQueue中獲取任務並處理,若是獲取步到則進入wait隊列
3.TaskQueue中添加新的任務後,喚醒wait隊列中的Consumer
4.被喚醒後的Consumer進入執行任務隊列,從新獲取任務並執行

代碼參考:編程

import java.util.Random;
import java.util.Vector;

/** * @ClassName WaitDemo2 * @Description TODO * @Author 鏗然一葉 * @Date 2019/10/3 11:43 * @Version 1.0 * javashizhan.com **/
public class WaitDemo2 {

    public static void main(String[] args) {
        //初始化任務隊列
        TaskQueue taskQueue = new TaskQueue();

        //啓動任務consumer
        for (int i = 0; i < 4; i++) {
            new Thread(new Consumer(taskQueue)).start();
        }

        //休眠一段時間等到consumer都啓動好
        sleep(2000);

        //啓動任務生產者Producer
        new Thread(new Producer(taskQueue)).start();
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//任務生產者
class Producer implements Runnable {

    private TaskQueue taskQueue;

    public Producer(TaskQueue taskQueue) {
        this.taskQueue = taskQueue;
    }

    public void run() {
        while(true) {
            generateTask();
            sleep(2000);
        }
    }

    //生成任務
    private void generateTask() {
        int taskNum = (int)(Math.random()*5+1);
        long timestamp = System.currentTimeMillis();
        for (int i = 0; i < taskNum; i++) {
            String task = "Task_" + timestamp + "_" + i;
            taskQueue.addTask(task);
        }
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//任務消費者
class Consumer implements Runnable {

    private TaskQueue taskQueue;

    public Consumer(TaskQueue taskQueue) {
        this.taskQueue = taskQueue;
    }

    public void run() {
        execTask();
    }

    private void execTask() {
        while (true) {
            //獲取任務,若是獲取不到,會進入wait隊列
            String task = taskQueue.removeTask();
            //任務不爲null則模擬執行
            if (null != task) {
                System.out.println(task + " be done. Caller is " + Thread.currentThread().getName());
            }
        }
    }
}

//任務隊列
class TaskQueue {

    private Vector<String> taskVector = new Vector<String>();

    //添加任務
    public synchronized void addTask(String task) {
        System.out.println(task + " has generated.");
        taskVector.add(task);
        //喚醒Consumer
        this.notify();
    }

    //移除任務
    public synchronized String removeTask() {
        if (!taskVector.isEmpty()) {
            return taskVector.remove(0);
        } else {
            try {
                System.out.println(Thread.currentThread().getName() + " waiting...");
                //沒有任務則進入等待隊列
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return null;
    }
}
複製代碼

運行日誌:緩存

Thread-0 waiting...
Thread-3 waiting...
Thread-2 waiting...
Thread-1 waiting...
Task_1570104227120_0 has generated.
Task_1570104227120_1 has generated.
Task_1570104227120_0 be done. Caller is Thread-0
Task_1570104227120_2 has generated.
Task_1570104227120_3 has generated.
Task_1570104227120_1 be done. Caller is Thread-3
Task_1570104227120_3 be done. Caller is Thread-1
Thread-2 waiting...
Task_1570104227120_2 be done. Caller is Thread-0
Thread-1 waiting...
Thread-3 waiting...
Thread-0 waiting...
Task_1570104229120_0 has generated.
Task_1570104229120_1 has generated.
Task_1570104229120_2 has generated.
Task_1570104229120_3 has generated.
Task_1570104229120_4 has generated.
Task_1570104229120_0 be done. Caller is Thread-2
Task_1570104229120_2 be done. Caller is Thread-2
Task_1570104229120_4 be done. Caller is Thread-2
Thread-2 waiting...
Task_1570104229120_1 be done. Caller is Thread-0
Thread-1 waiting...
Thread-0 waiting...
Task_1570104229120_3 be done. Caller is Thread-3
Thread-3 waiting...
Task_1570104231121_0 has generated.
Task_1570104231121_1 has generated.
Task_1570104231121_2 has generated.
Task_1570104231121_0 be done. Caller is Thread-2
Thread-2 waiting...
Task_1570104231121_1 be done. Caller is Thread-0
Thread-0 waiting...
Task_1570104231121_2 be done. Caller is Thread-1
Thread-1 waiting...
複製代碼

從日誌能夠看出,生成的任務數和線程被調用次數是相等的。安全

2、TaskQueue中的this.wait發生了什麼

咱們經過一副圖來理解this.wait:bash


1.wait操做將調用線程放入wait隊列中,等待喚醒。這裏的調用線程不是TaskQueue,而是調用了removeTask()方法的Consumer。
2.wait隊列歸屬一個對象,這裏是this,而this是TaskQueue的一個實例對象,所以這個wait隊列歸屬一個TaskQueue實例。

注:不少人容易犯的錯誤是誰調用了wait,那麼誰就進入wait隊列,而實際上進入wait隊列的應該是調用線程,而不是以下的obj。併發

經過jstack命令查看堆棧信息能夠驗證這一點:app

2019-10-03 13:04:52
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.40-b25 mixed mode):

"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x00000000035b2800 nid=0x8fe0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-3" #15 prio=5 os_prio=0 tid=0x000000001f1cd800 nid=0x324c in Object.wait() [0x00000000200ae000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076bedff18> (a com.javashizhan.concurrent.demo.base.TaskQueue)
	at java.lang.Object.wait(Object.java:502)
	at com.javashizhan.concurrent.demo.base.TaskQueue.removeTask(WaitDemo2.java:108)
	- locked <0x000000076bedff18> (a com.javashizhan.concurrent.demo.base.TaskQueue)
	at com.javashizhan.concurrent.demo.base.Consumer.execTask(WaitDemo2.java:84)
	at com.javashizhan.concurrent.demo.base.Consumer.run(WaitDemo2.java:79)
	at java.lang.Thread.run(Thread.java:745)

"Thread-2" #14 prio=5 os_prio=0 tid=0x000000001f1cd000 nid=0x84d0 in Object.wait() [0x000000001ffaf000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076bedff18> (a com.javashizhan.concurrent.demo.base.TaskQueue)
	at java.lang.Object.wait(Object.java:502)
	at com.javashizhan.concurrent.demo.base.TaskQueue.removeTask(WaitDemo2.java:108)
	- locked <0x000000076bedff18> (a com.javashizhan.concurrent.demo.base.TaskQueue)
	at com.javashizhan.concurrent.demo.base.Consumer.execTask(WaitDemo2.java:84)
	at com.javashizhan.concurrent.demo.base.Consumer.run(WaitDemo2.java:79)
	at java.lang.Thread.run(Thread.java:745)

"Thread-1" #13 prio=5 os_prio=0 tid=0x000000001f1cb000 nid=0x4404 in Object.wait() [0x000000001feae000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076bedff18> (a com.javashizhan.concurrent.demo.base.TaskQueue)
	at java.lang.Object.wait(Object.java:502)
	at com.javashizhan.concurrent.demo.base.TaskQueue.removeTask(WaitDemo2.java:108)
	- locked <0x000000076bedff18> (a com.javashizhan.concurrent.demo.base.TaskQueue)
	at com.javashizhan.concurrent.demo.base.Consumer.execTask(WaitDemo2.java:84)
	at com.javashizhan.concurrent.demo.base.Consumer.run(WaitDemo2.java:79)
	at java.lang.Thread.run(Thread.java:745)

"Thread-0" #12 prio=5 os_prio=0 tid=0x000000001f0d8800 nid=0x6750 in Object.wait() [0x000000001fdae000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076bedff18> (a com.javashizhan.concurrent.demo.base.TaskQueue)
	at java.lang.Object.wait(Object.java:502)
	at com.javashizhan.concurrent.demo.base.TaskQueue.removeTask(WaitDemo2.java:108)
	- locked <0x000000076bedff18> (a com.javashizhan.concurrent.demo.base.TaskQueue)
	at com.javashizhan.concurrent.demo.base.Consumer.execTask(WaitDemo2.java:84)
	at com.javashizhan.concurrent.demo.base.Consumer.run(WaitDemo2.java:79)
	at java.lang.Thread.run(Thread.java:745)

"Service Thread" #11 daemon prio=9 os_prio=0 tid=0x000000001f123800 nid=0x60ac runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000000001f099000 nid=0x8094 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000001f084800 nid=0x91d0 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000000001f07f000 nid=0x8f44 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000000001f07b000 nid=0x9034 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000001f056800 nid=0x47dc runnable [0x000000001f6ae000]
   java.lang.Thread.State: RUNNABLE
	at java.net.SocketInputStream.socketRead0(Native Method)
	at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
	at java.net.SocketInputStream.read(SocketInputStream.java:170)
	at java.net.SocketInputStream.read(SocketInputStream.java:141)
	at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
	at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
	at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
	- locked <0x000000076c010b70> (a java.io.InputStreamReader)
	at java.io.InputStreamReader.read(InputStreamReader.java:184)
	at java.io.BufferedReader.fill(BufferedReader.java:161)
	at java.io.BufferedReader.readLine(BufferedReader.java:324)
	- locked <0x000000076c010b70> (a java.io.InputStreamReader)
	at java.io.BufferedReader.readLine(BufferedReader.java:389)
	at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000001efe9000 nid=0x8514 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001f038000 nid=0x861c runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x00000000036aa000 nid=0x6f48 in Object.wait() [0x000000001efaf000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076bc06f58> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
	- locked <0x000000076bc06f58> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x00000000036a3000 nid=0x6e7c in Object.wait() [0x000000001eeaf000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076bc06998> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:502)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157)
	- locked <0x000000076bc06998> (a java.lang.ref.Reference$Lock)

"VM Thread" os_prio=2 tid=0x000000001cfea000 nid=0x1780 runnable 

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00000000035c8000 nid=0x8a28 runnable 

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00000000035c9800 nid=0x8e94 runnable 

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00000000035cb000 nid=0x9128 runnable 

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00000000035cd800 nid=0x8f60 runnable 

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x00000000035d0000 nid=0xec0 runnable 

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x00000000035d1000 nid=0x9100 runnable 

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x00000000035d4000 nid=0x4104 runnable 

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x00000000035d5800 nid=0x6f44 runnable 

"VM Periodic Task Thread" os_prio=2 tid=0x000000001f0d7000 nid=0x7978 waiting on condition 

JNI global references: 22
複製代碼

以下代碼說明是線程Consumer進入了wait隊列:dom

"Thread-3" #15 prio=5 os_prio=0 tid=0x000000001f1cd800 nid=0x324c in Object.wait() [0x00000000200ae000]
   java.lang.Thread.State: WAITING (on object monitor)
複製代碼

3、notify和notifyAll

1.notify喚醒隊列中的一個等待對象
2.notifyAll喚醒隊列中的全部等待對象socket

在上述例子中任務是一個個添加的,所以調用notify沒有問題;若是批量添加任務,只調用一次notify,那麼就可能出現只有一個consumer被喚醒處理任務,其餘consumer被餓死;而若是添加一個任務就調用notifyAll,那麼會無謂的喚醒多餘的Consumer,沒有任務可執行的Consumer被喚醒後,又當即進入wait隊列。post

不少時候了避免consumer被意外餓死,保險起見都統一調用notifyAll而不是notify,實際也不至於都如此,只要理解了原理,合理分析就能夠知道應該調用哪一個。

判斷使用notify的依據有:
1.全部等待線程擁有相同的等待條件;
2.全部等待線程被喚醒後,執行相同的操做;
3.只須要喚醒一個線程。

4、wait和sleep

1.wait會釋放鎖而sleep不會
2.wait只能在synchronized代碼塊中執行,而sleep沒有限制
3.wait的使用更像事件監聽機制,工做線程監聽某個事件(如任務隊列),事件到達後通知工做線程,而sleep的使用更像輪詢機制,不斷的輪詢任務隊列中是否又任務。在處理任務隊列這個場景上使用wait更優一些。

5、總結

1.wait和notify,notifyAll只能出如今synchronized代碼塊中
2.obj.wait()方法基於obj對象生成了一個wait隊列
3.調用obj.wait的同步代碼塊的線程進入了等待隊列,而不是obj進入等待隊列
4.使用notify和notifyAll要根據實際場景具體分析
5.任務隊列場景wait優於sleep,可避免沒必要要的輪詢。

end.


相關閱讀:
Java併發編程(一)知識地圖
Java併發編程(二)原子性
Java併發編程(三)可見性
Java併發編程(四)有序性
Java併發編程(五)建立線程方式概覽
Java併發編程入門(六)synchronized用法
Java併發編程入門(八)線程生命週期
Java併發編程入門(九)死鎖和死鎖定位
Java併發編程入門(十)鎖優化
Java併發編程入門(十一)限流場景和Spring限流器實現
Java併發編程入門(十二)生產者和消費者模式-代碼模板
Java併發編程入門(十三)讀寫鎖和緩存模板
Java併發編程入門(十四)CountDownLatch應用場景
Java併發編程入門(十五)CyclicBarrier應用場景
Java併發編程入門(十六)秒懂線程池差異
Java併發編程入門(十七)一圖掌握線程經常使用類和接口
Java併發編程入門(十八)再論線程安全


Java極客站點: javageektour.com/

相關文章
相關標籤/搜索