1. 程式人生 > >Java基礎之多執行緒案例-單生產單消費

Java基礎之多執行緒案例-單生產單消費

在講單生產單消費之前,我們先來說一下執行緒間通訊的問題

一、 執行緒間通訊

概念:多個執行緒在處理同一個資源,但是處理的動作(執行緒的任務)卻不相同。
比如:執行緒A用來生成包子的,執行緒B用來吃包子的,包子可以理解為同一資源,執行緒A與執行緒B處理的動作,一個是生產,一個是消費,那麼執行緒A與執行緒B之間就存線上程通訊問題。
這裡寫圖片描述
為什麼要處理執行緒間通訊
多個執行緒併發執行時, 在預設情況下CPU是隨機切換執行緒的,當我們需要多個執行緒來共同完成一件任務,並且我們希望他們有規律的執行, 那麼多執行緒之間需要一些協調通訊,以此來幫我們達到多執行緒共同操作一份資料。
如何保證執行緒間通訊有效利用資源


多個執行緒在處理同一個資源,並且任務不同時,需要執行緒通訊來幫助解決執行緒之間對同一個變數的使用或操作。 就是多個執行緒在操作同一份資料時, 避免對同一共享變數的爭奪。也就是我們需要通過一定的手段使各個執行緒能有效的利用資源。而這種手段即—— 等待喚醒機制

二、 等待喚醒機制

什麼是等待喚醒機制
這是多個執行緒間的一種協作機制。談到執行緒我們經常想到的是執行緒間的競(race),比如去爭奪鎖,但這並不是故事的全部,執行緒間也會有協作機制。就好比在公司裡你和你的同事們,你們可能存在在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。就是在一個執行緒進行了規定操作後,就進入等待狀態(wait()), 等待其他執行緒執行完他們的指定程式碼過後 再將其喚醒(notify());在有多個執行緒進行等待時, 如果需要,可以使用 notifyAll()來喚醒所有的等待執行緒。wait/notify

就是執行緒間的一種協作機制。
等待喚醒中的方法
等待喚醒機制就是用於解決執行緒間通訊的問題的,使用到的3個方法的含義如下:

wait() // 執行緒不再活動,不再參與排程,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的執行緒狀態即是 WAITING。它還要執行一個特別的動作,也即是“通知(notify)”在這個物件上等待的執行緒從wait set 中釋放出來,重新進入到排程佇列(ready queue)中。
notify() // 則選取所通知物件的 wait set 中的一個執行緒釋放;例如,餐館有空位置後,等候就餐最久的顧客最先入座。
notifyAll()
// 則釋放所通知物件的 wait set 上的全部執行緒。

:哪怕只通知了一個等待的執行緒,被通知執行緒也不能立即恢復執行,因為它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它執行緒的競爭),成功後才能在當初呼叫 wait 方法之後的地方恢復執行。
呼叫wait和notify方法需要注意的細節
1. wait方法與notify方法必須要由同一個鎖物件呼叫。因為:對應的鎖物件可以通過notify喚醒使用同一個鎖物件呼叫的wait方法後的執行緒。
2. wait方法與notify方法是屬於Object類的方法的。因為:鎖物件可以是任意物件,而任意物件的所屬類都是繼承了Object類的。
3. wait方法與notify方法必須要在同步程式碼塊或者是同步函式中使用。因為:必須要通過鎖物件呼叫這2個方法。
4. lock方法與unlock方法有與其匹配的方法。(可去百度,在這就不詳細說了)

舉例說明

package cn.itcast.day07.demo01;

/*
public void wait():釋放鎖,進入WAITING狀態。無限等待。
public void wait(long time):釋放鎖,進入TIMED_WAITING狀態。定時等待。
public void notify():不釋放鎖,發通知讓處於等待狀態的其他【一個】執行緒結束等待。
public void notifyAll():不釋放鎖,發通知讓處於等待狀態的其他【所有】執行緒結束等待。
 */
public class Demo01NotifyAll {

    public static void main(String[] args) {
        Object lock = new Object();

        Runnable task = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                synchronized (lock) {
                    System.out.println(name + "獲取了鎖,即將釋放並進入WAITING狀態……");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "醒來並獲取到了鎖,將在3秒之後釋放");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "釋放了鎖");
                } // sync
            }
        };

        new Thread(task, "[A1]").start();
        new Thread(task, "[A2]").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock) {
                    System.out.println(name + "獲取了鎖,通知所有人");
                    lock.notifyAll();
                    System.out.println(name + "即將在3秒之後釋放鎖");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(name + "釋放掉鎖");
                } // sync
            }
        }, "[B]").start();
    }

}

程式碼中共三個執行緒,分別是[A1]、[A2]、[B],此程式碼共執行10s鍾(只包含程式開始之後,不包含編譯之類的),程式碼執行模型如下:
這裡寫圖片描述

三、單生產與單消費問題

這是我們今天的重頭戲。等待喚醒機制其實就是經典的“生產者與消費者”的問題。
就拿生產包子消費包子來說等待喚醒機制如何有效利用資源:
包子鋪執行緒生產包子,吃貨執行緒消費包子。當包子沒有時(包子狀態為false),吃貨執行緒等待,包子鋪執行緒生產包子(即包子狀態為true),並通知吃貨執行緒(解除吃貨的等待狀態),因為已經有包子了,那麼包子鋪執行緒進入等待狀態。接下來,吃貨執行緒能否進一步執行則取決於鎖的獲取情況。如果吃貨獲取到鎖,那麼就執行吃包子動作,包子吃完(包子狀態為false),並通知包子鋪執行緒(解除包子鋪的等待狀態),吃貨執行緒進入等待。包子鋪執行緒能否進一步執行則取決於鎖的獲取情況。

顧客:

package cn.itcast.day07.demo01;

public class SingleConsumer implements Runnable {

    private final Object LOCK;

    public SingleConsumer(Object lock) {
        this.LOCK = lock;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        while (true) {
            synchronized (LOCK) {
                if (SingleMain.products <= 0) { // 這裡條件判斷使用的是if,其實使用while也可以
                    System.out.println(name + "因為沒有包子,等待……");
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(name + "吃掉了包子:" + SingleMain.products--);
                LOCK.notify(); // 通知對方廚子趕緊做
            } // sync
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } // while
    }
}

廚子:

package cn.itcast.day07.demo01;

public class SingleProducer implements Runnable {

    private final Object LOCK;

    public SingleProducer(Object lock) {
        this.LOCK = lock;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        while (true) {
            synchronized (LOCK) {
                if (SingleMain.products >= 100) { // 這裡條件判斷使用的是if,其實使用while也可以
                    System.out.println(name + "因為包子太多,等待……");
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(name + "做出了包子:" + ++SingleMain.products);
                LOCK.notify(); // 通知對方顧客趕緊吃
            } // sync
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } // while
    }
}

Main方法:

package cn.itcast.day07.demo01;

public class SingleMain {

    public static int products = 50; // 初始50個包子

    public static void main(String[] args) {
        Object lock = new Object();

        new Thread(new SingleConsumer(lock), "[顧客]").start();
        new Thread(new SingleProducer(lock), "[廚子]").start();
    }

}

下圖是單生產單消費模型:

這裡寫圖片描述
好,到這裡就結束了,在下一篇部落格我將介紹多生產多消費問題,( ^_^ )/~~拜拜。