【併發編程】 --- 線程間的通信wait、notify、notifyAll

源碼地址:https://github.com/nieandsun/concurrent-study.git


1 wait、notify、notifyAll簡單介紹


1.1 使用方法 + 爲什麼不是Thread類的方法

爲什麼不是Thread類的方法

首先應該明確wait、notify、notifyAll三個方法都是對鎖對象的操作,而鎖可以是任何對象。在java的世界中任何對象都屬於Object類,因此這三個方法都是Object的方法, 而不是線程對象Thread的方法。

使用方法

需要注意兩點:

  • (1)這三個方法必須在synchronized關鍵字包含的臨界區(簡單理解,就是代碼塊)內使用
  • (2)使用方式爲鎖對象.方法(),比如obj.wait();

1.2 什麼時候加鎖、什麼時候釋放鎖?

必須要明確以下幾點:

  • (1)notify和notifyAll方法不會釋放鎖,這兩個方法只是通知其他使用該鎖當鎖但是在wait狀態的線程,可以準備搶鎖了
    • 這裏還要格外注意一點,其他使用該鎖當鎖且處於wait狀態的線程只有被notify或notifyAll喚醒了,纔有資格搶鎖
  • (2)某個鎖對象調用wait方法會立即釋放當前線程的該對象鎖 , 且其他線程通過notify/notifyAll方法通知該線程可以搶該對象鎖時,如果當前線程搶到了,會從當前鎖的wait方法之後開始執行 — 即從哪裏wait,從哪裏執行;
  • (3)在synchronized、wait、notify、notifyAll的組合裏
    • 加鎖的方式只有一個即進入同步代碼塊時加鎖;
    • 正常情況下釋放鎖的方式有兩個: ①鎖對象調用wait方法時會釋放鎖 ;② 走完同步代碼塊時自動釋放鎖
      • 還有一種釋放鎖的時機: 同步代碼塊裏出現異常。

1.3 notify、notifyAll的區別

  • 某個鎖對象的notify只會喚醒一個使用該鎖當鎖且處於wait狀態的線程;
  • 某個鎖對象的notifyAll方法會把所有使用該鎖當鎖且處於wait狀態的線程都喚醒;

使用建議: 爲了防止某些線程無法被通知到,建議都使用notifyAll。


2 兩個比較經典的使用案例

感覺上學的時候好像就考過下面這兩個案例☺☺☺


2.1 案例1 — ABCABC。。。三個線程順序打印問題


2.1.1 題目

三個線程,線程A不停打印A、線程B不停的打印B、線程C不停的打印C,如何通過synchronized、wait、notifyAll(或notify)的組合,使三個線程不停地且順序地打印出ABCABC。。。


2.1.2 題目分析

其實我在《【併發編程】— Thread類中的join方法》這篇文章裏用join實現過類似的功能,有興趣的可以看一下。。。

如果使用synchronized、wait、notifyAll(或notify)的組合的話,這個問題可以歸結爲下圖所示的問題。即:

線程A走完 ,線程B走 —> 線程B走完,線程C走 —》 線程C走完,線程A走 。。。。

在這裏插入圖片描述
以線程A爲起點進行分析,可知:

  • (1)要想線程A走完,線程B接着走,那肯定是線程A釋放了線程B所需要的鎖,這裏設該鎖爲U,做進一步分析可知:

    • 既然線程B需要線程A釋放的鎖U,那就意味着此時線程B中的鎖U肯定處於wait狀態;
    • 同時要想線程A釋放了鎖U之後,線程B可以被喚醒,線程A還必須得進行鎖U的notify或notifyAll
  • (2)同理,要想線程B走完,線程C走,那肯定是線程C有一把處於wait狀態的鎖,這裏設爲V,需要線程B進行該鎖的notify或notifyAll 並釋放

  • (3)再同理,要想線程C走完,線程A接着走,那肯定是線程A有一把處於wait的鎖,這裏設爲W,需要線程C進行該鎖的notify或notifyAll 並釋放

用圖可以表示成下面的樣子:

在這裏插入圖片描述
分析到這裏我們可以再提煉一下:

  • (1)每個線程都應該有兩把鎖
  • (2)第一把鎖是前面的線程釋放後自己要搶到的鎖、第二把鎖是自己要notify或notifyAll的鎖,對應到每個線程,就可以這樣描述
    • 線程A需要兩把鎖,一把爲線程C需要notify(或notifyAll)+ 釋放的鎖,可以認爲該鎖爲C鎖;另一把是自己需要notify(或notifyAll)+釋放的鎖,可以認爲該鎖爲A鎖
    • 同理,線程B需要A線程notify(或notifyAll)+ 釋放的鎖A鎖,自己需要notify(或notifyAll)+釋放的B鎖
    • 再同理,線程C需要B線程notify(或notifyAll)+ 釋放的鎖B鎖,自己需要notify(或notifyAll)+釋放的C鎖

分析到這裏後,可以將上圖改成下面的樣子,這樣理解起來,我感覺會更好一些:

在這裏插入圖片描述

分析到這裏就可以寫代碼了。


2.1.3 我的答案

  • code
package com.nrsc.ch1.base.producer_consumer.ABCABC;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
@Slf4j
@AllArgsConstructor
public class ABCABC implements Runnable {

    private String obj;
    //前一個線程需要釋放,本線程需要wait的鎖
    private Object prev;
    //本線程需要釋放,下一個線程需要wait的鎖
    private Object self;


    @Override
    public void run() {
        int i = 3;
        while (i > 0) {
            //爲了在控制檯好看到效果,我這裏打印3輪
            synchronized (prev) { //搶前面線程的鎖
                synchronized (self) {// 搶到自己應該釋放的鎖
                    System.out.println(obj);
                    i--;
                    self.notifyAll(); //喚醒其他線程搶self
                }//釋放自己應該釋放的鎖

                try {
                    //走到這裏本線程已經釋放了自己應該釋放的鎖,接下來就需要讓自己需要等待的鎖進行等待就可以了
                    if (i > 0) { //我最開始沒加這個條件,但是測試發現程序沒停,其實分析一下就可以知道
                        //當前面i--使i=0了,其實該線程就已經完成3次打印了,就不需要再等前面的鎖了
                        //因此這裏加了該if判斷
                        prev.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object lockA = new Object();
        Object lockB = new Object();
        Object lockC = new Object();
        //線程A需要等待C線程釋放的鎖,同時需要釋放本線程該釋放的鎖A
        new Thread(new ABCABC("A", lockC, lockA)).start();
        Thread.sleep(1); //確保開始時A線程先執行

        //線程B需要等待A線程釋放的鎖,同時需要釋放本線程該釋放的鎖B
        new Thread(new ABCABC("B", lockA, lockB)).start();
        Thread.sleep(1); //確保開始時B線程第2個執行

        //線程C需要等待B線程釋放的鎖,同時需要釋放本線程該釋放的鎖C
        new Thread(new ABCABC("C", lockB, lockC)).start();

    }
}
  • 測試結果:

在這裏插入圖片描述


2.2 生產者消費者問題


2.2.1 題目

如下圖所示:

  • (1)有多個生產者,每個生產者都在不斷的搶麪包廠裏的機器生產麪包 —> 某個時間段只能有一個生產者進行生產
  • (2)廠裏最多能存儲20箱,也就是說當已經有20箱了,各個生產者就不能生產了,需要等待消費者消費了,才能繼續生產
  • (3)消費者也有多個,他們也會搶着去麪包廠買麪包,但也是某個時間段,只能有一個消費者搶到買麪包的資格

在以上條件的基礎上,寫一個多線程程序,保證在生產者不斷生產麪包的同時,消費者也在不斷的購買麪包。
注意: 不能寫成生產者先生產了20箱,然後消費者再去消費20箱)
在這裏插入圖片描述


2.2.2 題目分析

其實我覺得這個很簡單,只需要想明白下面的兩點肯定就可以把這個代碼寫出來。

對於生產者

  • (1)它們要不停地生產,直到麪包的箱數大於等於20時,生產者就等待 —> 等着消費者去消費
  • (2)當面包的箱數小於20時,搶到生產權的生產者就生產,並通知消費者,我剛生產了一個,你們可以再繼續消費了

對於消費者

  • (1)他們要不停地消費,知道麪包的箱數爲0時,它們就等待 —> 等着生產這去生產
  • (2)當面包的箱數大於0時,搶到消費權的消費者就消費,並通知生產者,我剛消費了一個,你們可以再繼續生產了

2.2.3 我的答案

  • 生產者和消費者
package com.nrsc.ch1.base.producer_consumer.multi;

import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BreadProducerAndConsumer2 {
    /***麪包集合*/
    private int i = 0;

    /***
     * 生產者 ,注意這裏鎖是當前對象,即this
     */
    public synchronized void produceBread() {
        //如果大於等於20箱,就等待  --- 如果這裏爲大於20的話,則20不會進入while,則會生產出21箱,所以這裏應爲>=
        while (i >= 20) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                log.error("生產者{},等待出錯", Thread.currentThread().getName(), e);
            }
        }

        //如果不到20箱就繼續生產
        i++; //生產一箱
        log.warn("{}生產一箱麪包,現有面包{}個", Thread.currentThread().getName(), i);
        //生產完,通知消費者進行消費
        this.notifyAll();
    }


    /***
     * 消費者
     */
    public synchronized void consumeBread() {

        //如果沒有了就等待
        while (i <= 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                log.error("消費者{},等待出錯", Thread.currentThread().getName(), e);
            }
        }
        //能走到這裏說明i>0,所以進行消費
        i--; //消費一箱
        log.info("{}消費一個麪包,現有面包{}個", Thread.currentThread().getName(), i);
        //消費完,通知生產者進行生產
        this.notifyAll();
    }
}
  • 測試類
package com.nrsc.ch1.base.producer_consumer.multi;
public class MultiTest {


    public static void main(String[] args) throws InterruptedException {

        BreadProducerAndConsumer2 pc = new BreadProducerAndConsumer2();

        /***
         * 不睡眠幾秒,效果不是很好,
         * 因此我在
         *  生產者線程裏睡了12秒 --- 因爲我覺得生產麪包的時間應該長 ☻☻☻
         *  消費者線程裏睡了6秒  --- 因爲我覺得買麪包的時間應該快  ☻☻☻
         */

        //生產者線程
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                //每個線程都不停的生產
                while (true) {
                    try {
                        Thread.sleep(12);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pc.produceBread();
                }
            }, "生產者" + i).start();
        }


        //消費者線程
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                //每個線程都不停的消費
                while (true) {
                    try {
                        Thread.sleep(6);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pc.consumeBread();
                }
            }, "消費者" + i).start();
        }
    }

}
  • 測試效果如下:

在這裏插入圖片描述

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