1. 程式人生 > >非同步程式設計學習之路(三)-多執行緒之間的協作與通訊

非同步程式設計學習之路(三)-多執行緒之間的協作與通訊

本文是非同步程式設計學習之路(三)-多執行緒之間的協作與通訊,若要關注前文,請點選傳送門:

非同步程式設計學習之路(二)-通過Synchronize實現執行緒安全的多執行緒

通過前文,我們學習到如何實現同步的多執行緒,但是在很多情況下,僅僅同步是不夠的,還需要執行緒與執行緒協作(通訊),生產者/消費者問題是一個經典的執行緒同步以及通訊的案例。該問題描述了兩個共享固定大小緩衝區的執行緒,即所謂的“生產者”和“消費者”在實際執行時會發生的問題。生產者的主要作用是生成一定量的資料放到緩衝區中,然後重複此過程。與此同時,消費者也在緩衝區消耗這些資料。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入資料,消費者也不會在緩衝區中空時消耗資料。要解決該問題,就必須讓生產者在緩衝區滿時休眠(要麼乾脆就放棄資料),等到下次消費者消耗緩衝區中的資料的時候,生產者才能被喚醒,開始往緩衝區新增資料。同樣,也可以讓消費者在緩衝區空時進入休眠,等到生產者往緩衝區新增資料之後,再喚醒消費者,通常採用執行緒間通訊的方法解決該問題。如果解決方法不夠完善,則容易出現死鎖的情況。出現死鎖時,兩個執行緒都會陷入休眠,等待對方喚醒自己。該問題也能被推廣到多個生產者和消費者的情形。

假設有這樣一種情況,有一個西餐廳,廚師負責製作牛排並放到顧客的盤子裡,顧客負責享用盤子裡的牛排,A執行緒可以看做廚師,B執行緒可以看做是顧客,如果盤子裡面沒有牛排,則B執行緒進入阻塞佇列,此時喚醒A執行緒製作牛排,如果盤子裡有牛排,則A執行緒進入阻塞佇列,此時喚醒B執行緒享用牛排,如此迴圈有序的進行。程式碼如下:

/**
 * @Description:多執行緒之間的協作與通訊
 * @Author:zhangzhixiang
 * @CreateDate:2018/12/21 12:53:36
 * @Version:1.0
 */
public class Plate {

    private List<Object> foods = new ArrayList<>();

    public synchronized void enjoy() {
        while (foods.size() == 0) {
            try {
                wait();
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
        }
        Object food = foods.get(0);
        foods.clear();
        notify();//喚醒阻塞佇列執行緒到就緒佇列
        System.out.println(String.format("顧客正在享受%s,好吃點贊。", food));
    }

    public synchronized void cooking() {
        while (foods.size() > 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Object food = "牛排";
        foods.add(food);
        notify();//喚醒阻塞佇列執行緒到就緒佇列
        System.out.println(String.format("廚師製作%s,並放到顧客的盤子裡。", food));
    }

    public static void main(String[] args) {
        Plate plate = new Plate();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> plate.cooking()).start();
            new Thread(() -> plate.enjoy()).start();
        }
    }

}

執行結果:

廚師製作牛排,並放到顧客的盤子裡。
顧客正在享受牛排,好吃點贊。
廚師製作牛排,並放到顧客的盤子裡。
顧客正在享受牛排,好吃點贊。
廚師製作牛排,並放到顧客的盤子裡。
顧客正在享受牛排,好吃點贊。
廚師製作牛排,並放到顧客的盤子裡。
顧客正在享受牛排,好吃點贊。
廚師製作牛排,並放到顧客的盤子裡。
顧客正在享受牛排,好吃點贊。
廚師製作牛排,並放到顧客的盤子裡。
顧客正在享受牛排,好吃點贊。
廚師製作牛排,並放到顧客的盤子裡。
顧客正在享受牛排,好吃點贊。
廚師製作牛排,並放到顧客的盤子裡。

以上程式碼有幾點需要注意:

1、執行結果中只打印了8次,並且程式還處於執行狀態?很明顯這裡出現了死鎖,A和B兩個執行緒都進入了休眠狀態,等待對方喚醒自己。

這裡的死鎖有三種解決方案:

(1)給wait()設定最大等待時間(ms),超過最大等待時間後對應執行緒會自動消亡。

    wait(10);//最大等待時間10ms

(2)將notify()改為notifyAll(),notify()和notifyAll的區別,前一個是喚醒阻塞佇列中的任意執行緒,後一個是喚醒就緒阻塞佇列中的全部執行緒。(synchronize中有兩個佇列,一個是阻塞佇列,一個是就緒佇列)。

    notifyAll();//喚醒阻塞佇列執行緒到就緒佇列

(3)通過Jdk中JUC包下的ReentrantLock和Condition來替換這裡的synchronize、wait、notify。(ReentrantLock和Condition在後文中會進行詳細講解)

2、Jdk5中notify()喚醒的是任意一個執行緒,這就說明有可能喚醒的是廚師執行緒,也有可能喚醒的是顧客執行緒,這就造成了一個喚醒的不明確性,你不知道喚醒的是什麼執行緒。在JDK7中給出瞭解決方案,Jdk7中可以根據業務通過Condition建立單獨的阻塞佇列,就比如這裡的廚師放到廚師的阻塞佇列中,顧客放到顧客的阻塞佇列中,這樣的話你也就知道喚醒的到底是什麼型別的執行緒了。

本文中提到了Jdk7中JUC包下的Lock鎖相關的方法,Lock相關介紹在後文中會進行詳細講解。本文中提到了wait、notify、notifyAll等多執行緒之間協作通訊的相關方法,下面的文章中我們會更詳細的介紹這些方法。

非同步程式設計學習之路(四)-睡眠、喚醒、讓步、合併