Java多線程拾遺(五)——notify和wait的一些事兒

前言

同樣參考《Java高併發編程詳解》一書這篇博客開始梳理notify和wait的一些事兒。

同步阻塞與異步非阻塞

我們之前總結過什麼是同步,什麼是阻塞。但並沒有總結清楚同步阻塞與異步非阻塞的東西。《Java高併發編程詳解》一書聊聊同步阻塞和異步非阻塞的區別,該書中通過一個實例說明了這兩者的區別。

同步阻塞

實例中,具體的需求也很簡單,就是客戶端提交一個Event類型的請求,交給服務器,服務器處理相關的請求,然後將處理結果返回給客戶端。同時我們可看到,在同步阻塞中。對於客戶端而言,提交了請求之後,需要一直等待服務端返回結果,這個期間客戶端無法做其他事情,只能等着服務端處理完相關數據,客戶端收到結果之後才能進行下一步操作。所以對於客戶端和服務端來說,這個請求是同步的,客戶端和服務端都得等着一個請求結束才能進行下一步,無法做到異步

對於客戶端來說,在沒有收到服務端的返回結果的時候,無法操作下一步,也只能等着,這個狀態稱爲阻塞中。因此這就是同步阻塞。可以看出,同步說的是客戶端和服務器端在同一個時間必須同步完成某一件事情,而阻塞只是相對於客戶端而言,兩者還是有區別的。

在這裏插入圖片描述

異步非阻塞

理解了同步阻塞,再來理解異步非阻塞,就容易的多了,所謂的異步,這裏指的是客戶端在發送請求之後,會立即受到服務端的一個返回。這個時候客戶端可以處理其他事情,客戶端的下一步操作並不依賴服務端的這個實時結果,如果後續客戶端需要結果了,再根據工單號調用相關的查詢接口獲取結果。服務端與客戶端處理數據的方式達到了異步,並不同步。這個時候,對於客戶端來說,並不用等服務端的結果了,因此不會因爲這個而阻塞,故而稱爲異步非阻塞。

在這裏插入圖片描述

wait和notify

乞丐版

乞丐版本的生產者和消費者

@Slf4j
public class ProduceAndConsumerVersion01 {
    
    private int i = 1;
    private final Object LOCK=new Object();

	//生產者
    private void produce(){
        synchronized (LOCK){
            log.info("P->,{}",i++);
        }
    }

	//消費者
    private void consume(){
        synchronized (LOCK){
            log.info("C->,{}",i);
        }
    }

    public static void main(String[] args) {
        ProduceAndConsumerVersion01 pc = new ProduceAndConsumerVersion01();
		//啓動生產者線程
        new Thread("P"){
            @Override
            public void run() {
                while(true){
                    pc.produce();
                }
            }
        }.start();
		//啓動消費者線程
        new Thread("C"){
            @Override
            public void run() {
                while(true){
                    pc.consume();
                }
            }
        }.start();
    }
}

一個線程生產數據,一個線程消費數據,且操作的都是同一個變量,邏輯上看似乎沒毛病,但是這個真就正常麼?

運行之後看到如下結果。

在這裏插入圖片描述

消費者永遠只是消費到最新的數據。這裏的根本原因就是生產者線程和消費者線程之間沒有一個通信機制,消費者不知道生產者生產了數據,生產者不知道消費者消費了數據。notify和wait就是幹這個的,就是線程間的一種通信方式。至少我們需要有一種方式,讓消費者知道生產者生產了數據,生產者知道消費者消費了數據。

notify和wait的引入

/**
 * autor:liman
 * createtime:2020/6/14
 * comment:生產者和消費者,
 */
@Slf4j
public class ProduceAndConsumerVersion02 {

    private int i = 0;

    private final Object LOCK = new Object();

    //引入一個標記,表示是否生產了數據
    private volatile boolean isProduced = true;

    public void produce() {
        synchronized (LOCK) {
            //如果存在數據,則阻塞
            if (isProduced) {
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            } else {//如果不存在數據,則需要生產數據
                i++;
                log.info("P->,{}", i);
                LOCK.notify();
                isProduced = true;
            }
        }
    }

    public void consume() {
        synchronized (LOCK) {
            //如果有數據,則需要消費數據
            if (isProduced) {
                log.info("C->,{}", i);
                LOCK.notify();
                isProduced = false;
            } else {//如果沒有數據,則需要阻塞,等待生產者產生數據
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            }
        }
    }

    public static void main(String[] args) {
        ProduceAndConsumerVersion02 pc = new ProduceAndConsumerVersion02();
        //啓動一個生產者
        Stream.of("P1").forEach(n -> {
            new Thread(n) {
                @Override
                public void run() {
                    while (true) {
                        pc.produce();
                    }
                }
            }.start();
        });
		//啓動一個消費者
        Stream.of("C1").forEach(n -> {
            new Thread(n) {
                @Override
                public void run() {
                    while (true) {
                        pc.consume();
                    }
                }
            }.start();
        });
    }
}

從運行結果來看,程序可以正常生產數據和消費數據。

在這裏插入圖片描述

如果有多個消費者,用如下的代碼,是不是正常的呢

package com.learn.thread.update.notifyandwait;

import lombok.extern.slf4j.Slf4j;

import java.util.stream.Stream;

/**
 * autor:liman
 * createtime:2020/6/14
 * comment:生產者和消費者,
 */
@Slf4j
public class ProduceAndConsumerVersion02 {

    private int i = 0;

    private final Object LOCK = new Object();

    //引入一個標記,表示是否生產了數據
    private volatile boolean isProduced = true;

    public void produce() {
        synchronized (LOCK) {
            //如果存在數據,則阻塞
            if (isProduced) {
                try {
                    log.info("生產者線程:{} 等待", Thread.currentThread().getName());
                    LOCK.wait();
                    log.info("生產者線程:{} 結束等待", Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            }
            //如果不存在數據,則需要生產數據
            i++;
            log.info("{} produce -> {}", Thread.currentThread().getName(), i);
            LOCK.notifyAll();
            isProduced = true;
        }
    }

    public void consume() {
        synchronized (LOCK) {
            if (!isProduced) {//如果沒有數據,則需要阻塞,等待生產者產生數據
                try {
                    log.info("消費者線程:{} 等待", Thread.currentThread().getName());
                    LOCK.wait();
                    log.info("消費者線程:{} 結束等待", Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            }
            //否則直接消費數據
            log.info("{} consume -> {}", Thread.currentThread().getName(), i);
            LOCK.notifyAll();
            isProduced = false;
        }
    }

    public static void main(String[] args) {
        ProduceAndConsumerVersion02 pc = new ProduceAndConsumerVersion02();
        //這裏構建多個生產者
        Stream.of("P1","P2").forEach(n -> {
            new Thread(n) {
                @Override
                public void run() {
                    while (true) {
                        pc.produce();
                    }
                }
            }.start();
        });
		//這裏構建多個消費者
        Stream.of("C1","C2").forEach(n -> {
            new Thread(n) {
                @Override
                public void run() {
                    while (true) {
                        pc.consume();
                    }
                }
            }.start();
        });
    }
}

運行結果:

在這裏插入圖片描述

發現程序卡住了,並沒有順利往下運行了,我們通過jstack命令查看到,發現我們創建的四個線程,四個都處於wait狀態,每個線程都不知道下一步該幹啥了,自然就無法正常運行了。其實解決方法也很簡單,將消費者線程或者生產者線程中的notify改成notifyAll即可

爲什麼會出現上述現象?可以參考如下的線程執行軌跡,P——生產者,C——消費者

1. P1 產生了一個數字1。
2. P2 想繼續產生數據,發現滿了,在wait裏面等了。
3. P3 想繼續產生數字,發現滿了,在 wait 裏面等了。
4. C1 想來消費數字,C2,C3 就在 get 裏面等着。
5. C1 開始執行,獲取1,然後調用 notify 然後退出。
如果 C1 把 C2 喚醒了,所以P2 (其他的都得等)只能在put方法上等着。(等待獲取synchoronized (this) 這個monitor)。
C2 檢查 while 循環發現並沒有任何數據,所以就在 wait 裏面等着。
C3 也比 P2 先執行,那麼發現依舊沒有數據,只能等着了。

6. 這時候我們發現 P2、C2、C3 都在等着鎖,最終 P2 拿到了鎖,放一個 1,notify,然後退出。
7. P2 這個時候喚醒了P3,P3發現隊已經存在數據了,沒辦法,只能等它變爲空。
8. 這時候沒有別的調用了,那麼現在這三個線程(P3, C2,C3)就全部變成 suspend 了,都在哪兒等着

分析了一下這個執行軌跡,會發現其實根本原因就在於有多個消費者和生產者的情況下,如果單純用notify,則會出現消費者或者生產者只是喚起了同類,就會出現上述情況,而notifyAll,就是喚起這個對象相關的所有處於wait狀態的線程。

如果將上述代碼改成notifyAll之後,多加幾個消費者,然後運行稍微久一點,可以發現上述代碼還有一個問題,出現多個消費者重複消費同一條數據。

在這裏插入圖片描述

這是爲啥?按照如下的運行軌跡來進行分析

1. C1 拿到了鎖進入同步代碼塊。
2. C1 發現沒有數據,然後進入等待,並釋放鎖 (wait方法是會釋放鎖的)。
3. 此時C2 拿到了鎖,發現 依舊沒有數據,然後進入等待,並釋放鎖 。
4. 這個時候有個線程P1往裏面加了個數據1,那麼notifyAll所有的等待的線程都被喚醒了。
5. C1,C2 重新獲取鎖,假設又是C1拿到了。然後他就走出if語句塊,消費了一個數據,沒有問題。
6. C1 移除數據後想通知別人,數據已經被消費了,於是調用了 notifyAll ,這個時候就把 C2 給喚醒了,那麼 C2 接着往下走。
7. 這時候 C2 就出問題了,因爲其實此時的競態條件已經不滿足了 (數據已經被消費了)。C2 以爲還可以從if語句正常出來,然後執行之後的語句,結果就重複消費了。

notifyAll雖然解決了所有線程等待的問題,但是如果不是在while中循環判斷,會出現重複消費的問題,因此正常的邏輯我們應該採用如下的代碼

/**
 * autor:liman
 * createtime:2020/6/14
 * comment: 生產者和消費者最終版本
 */
@Slf4j
public class ProduceAndConsumerVersion03 {

    private int i = 0;
    private final Object LOCK = new Object();
    private volatile boolean isProduced = true;

    public void produce() {
        synchronized (LOCK) {
            while (isProduced) {//這裏應該用循環去判斷。如果這裏用if,則後面需要補上else語句塊
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            }

            i++;
            log.info("p->,{}", i);
            LOCK.notifyAll();
            isProduced = true;
        }
    }

    public void consume() {
        synchronized (LOCK) {
            while (!isProduced) {//這裏應該用循環去判斷。如果這裏用if,則後面需要補上else語句塊
                try {
                    LOCK.wait();
                } catch (InterruptedException e) {
                    log.error("InterruptedException:{}", e);
                }
            }
            log.info("C->,{}", i);
            LOCK.notifyAll();
            isProduced = false;
        }
    }

    public static void main(String[] args) {
        ProduceAndConsumerVersion03 pc = new ProduceAndConsumerVersion03();
        Stream.of("P1").forEach(n -> new Thread(n) {
                    @Override
                    public void run() {
                        while (true) {
                            pc.produce();
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                log.error("InterruptedException:{}", e);
                            }
                        }
                    }
                }.start()
        );

        Stream.of("C1", "C2", "C3", "C4","C5").forEach(n -> new Thread(n) {
                    @Override
                    public void run() {
                        while (true) {
                            pc.consume();
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                log.error("InterruptedException:{}", e);
                            }
                        }
                    }
                }.start()
        );
    }
}

wait和sleep的區別

wait 方法

wait和notify方法其實並不是Thread特有的方法,而是屬於Object的方法,意味着程序中的任何一個對象都有wait方法,wait方法有三個重載的方法,具體如下

//前兩個方法底層調用的第三個方法
public final void wait() throws InterruptedException;//底層調用的是wait(0)
public final void wait(long timeout, int nanos) throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;//達到指定的時間,自動喚醒

1、當前線程執行了該對象的wait方法之後,會進入到該對象關聯的wait set中,這也意味着一旦線程執行了某個object的wait方法之後,就會釋放該對象的所有權(就是會釋放鎖),其他線程就有機會爭搶該鎖。

2、從實質來看,因爲wait與對象相關,因此執行這個方法必須要擁有指定對象的monitor(鎖標記),故而必須在同步方法或者同步代碼塊中使用該方法

3、notify是從對象的wait set中隨機喚醒一個處於阻塞狀態的線程,notifyAll則是喚醒wait set中的所有線程。

sleep方法

sleep方法是Thread的靜態方法,只是讓當前正在執行的線程,進入阻塞狀態,但是當前線程不會釋放鎖。

兩者區別

都是讓線程進入阻塞狀態,但是區別是很大的,也是面試中經常問的

不同點:

1、wait是屬於Object對象的方法,而sleep是屬於Thread的靜態方法

2、wait必須在同步代碼塊或者同步方法中調用,而sleep沒有這個限制

3、調用sleep的線程會在短暫休眠之後會主動退出阻塞,而調用wait方法的線程需要被其他線程喚醒才能退出阻塞

4、調用sleep的線程進入休眠的時候並不會釋放對應的鎖,而調用wait的方法會釋放對應的鎖

相同點:

1、都是可中斷方法(在之前的博客中介紹過:可中斷方法

2、都可以使線程進入阻塞狀態

一個綜合實例

/**
 * autor:liman
 * createtime:2020/6/16
 * comment:模擬多個線程的運行,但是同時控制正在運行的線程數不超過5個
 */
@Slf4j
public class SelfThreadGroupService {
    private final static int MAX_WROKERSIZE = 5;
    //沒有什麼實質作用,只是做一個大小的限制判斷
    final static private LinkedList<Control> CONTROLS = new LinkedList<>();

    public static void main(String[] args) {
        List<Thread> allWorkerThreadList = new ArrayList<>();
        //單純的啓動所有線程,並將線程翻入workerList
        Arrays.asList("M1","M2","M3","M4","M5","M6","M7","M8","M9","M10").stream()
                .map(SelfThreadGroupService::createWorkerThread)
                .forEach(t->{
                    t.start();
                    allWorkerThreadList.add(t);
                });

        //通過workerList進行join,因爲不能再start的時候進行join操作,否則會無法做到並行
        allWorkerThreadList.stream().forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                log.error("線程執行異常,異常信息爲:{}",e);
            }
        });
        log.info("所有的線程執行完畢");
    }

    private static Thread createWorkerThread(String name){
        return new Thread(()->{
            synchronized (CONTROLS) {
                //如果大於我們限制的數字,則等待
                while (CONTROLS.size() > MAX_WROKERSIZE) {
                    try {
                        CONTROLS.wait();
                    } catch (InterruptedException e) {
                        log.error("線程執行異常,異常信息爲:{}",e);
                    }
                }
                //沒有超限,則先進入到我們的緩存中
                CONTROLS.addLast(new Control());
            }

            //模擬當前線程的執行
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //執行完成之後,從緩存中移出
            synchronized (CONTROLS) {
                Optional.of("The worker [" + Thread.currentThread().getName() + "] END capture data.")
                        .ifPresent(System.out::println);
                CONTROLS.removeFirst();
                CONTROLS.notifyAll();
            }
        },name);
    }

    private static class Control {
    }

}

總結

本篇博客算是重新梳理了一下wait方法的作用與原理,希望能理解爲什麼wait方法必須要與一個對象關聯,wait與sleep的區別,以及notify與notifyAll帶來的一些問題,並且如何解決的,爲什麼wait建議一直在while判斷中。這些也是有些公司面試常考的東西。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章