1. 程式人生 > >第二十一講 多執行緒——多執行緒間的通訊——多個生產者和消費者

第二十一講 多執行緒——多執行緒間的通訊——多個生產者和消費者

首先,試著思考一下執行如下程式,看會得出什麼結果。

// 描述資源
class Res
{
    private String name; // 資源名稱
    private int count = 1; // 資源編號

    // 定義標記。
    private boolean flag;
    // 提供了給商品賦值的方法
    public synchronized void set(String name) //
    {
        if (flag) // 判斷標記為true,就執行wait()等待。為false,就生產。
            try{this.wait();}catch
(InterruptedException e){ } this.name = name + "--" + count; // 麵包1、麵包2、麵包3 count++; System.out.println(Thread.currentThread().getName() + "...生產者..." + this.name); // 生產完畢,將標記改為true。 flag = true; // 喚醒消費者。 this.notify(); } // 提供獲取一個商品的方法 public
synchronized void get() { if (!flag) try{this.wait();}catch(InterruptedException e){ } System.out.println(Thread.currentThread().getName() + ".......消費者......." + this.name); // 將標記改為false。 flag = false; // 喚醒生產者 this.notify(); } } // 生成者
class Producer implements Runnable { private Res r; Producer(Res r) { this.r = r; } public void run() { while (true) r.set("麵包"); } } // 消費者 class Consumer implements Runnable { private Res r; Consumer(Res r) { this.r = r; } public void run() { while (true) r.get(); } } class ProducerConsumerDemo3 { public static void main(String[] args) { // 1、建立資源 Res r = new Res(); // 2、建立兩個任務。 Producer pro = new Producer(r); Consumer con = new Consumer(r); // 3、建立執行緒 Thread t0 = new Thread(pro); Thread t1 = new Thread(pro); Thread t2 = new Thread(con); Thread t3 = new Thread(con); t0.start(); t1.start(); t2.start(); t3.start(); } }

從以上程式程式碼中我們可以看到有兩個生產者執行緒和兩個消費者執行緒,即多個生產者和多個消費者。執行以上程式,可以很清楚地看到所產生的問題:

  1. 重複生產 在這裡插入圖片描述

  2. 重複消費 在這裡插入圖片描述

我經過複雜的分析,

t0執行緒生產完麵包–1,繼續持有CPU的執行權,就等待了,此時t1、t2、t3執行緒具有資格; t1執行緒持有CPU的執行權,等待了,此時t2、t3執行緒具有資格; t2執行緒消費完麵包–1,持有CPU的執行權,就等待了,此時t0、t3執行緒具有資格; t3執行緒持有CPU的執行權,等待了,此時t0執行緒具有資格; t0執行緒生產完麵包–2,喚醒t1執行緒後繼續持有CPU的執行權,就等待了,此時t1執行緒具有資格; t1執行緒生產完麵包–3,…

發現被喚醒的執行緒沒有判斷標記就開始工作(生產or消費)了,從而導致了重複的生產和消費的發生。那麼如何來解決這個問題呢?很簡單,那就是被喚醒的執行緒必須判斷標記(可以使用while迴圈搞定)。如此一來,經過修改後的程式程式碼就變為:

// 描述資源
class Res
{
    private String name; // 資源名稱
    private int count = 1; // 資源編號

    // 定義標記。
    private boolean flag;
    // 提供了給商品賦值的方法
    public synchronized void set(String name) //
    {
        while (flag) // 判斷標記為true,就執行wait()等待。為false,就生產。
            try{this.wait();}catch(InterruptedException e){ }
        this.name = name + "--" + count; // 麵包1、麵包2、麵包3

        count++;

        System.out.println(Thread.currentThread().getName() + "...生產者..." + this.name); 
        // 生產完畢,將標記改為true。
        flag = true;

        // 喚醒消費者。
        this.notify();
    }

    // 提供獲取一個商品的方法
    public synchronized void get()
    {
        while (!flag)
            try{this.wait();}catch(InterruptedException e){ }
        System.out.println(Thread.currentThread().getName() + ".......消費者......." + this.name); 

        // 將標記改為false。
        flag = false;
        // 喚醒生產者
        this.notify();
    }
}

// 生成者
class Producer implements Runnable
{
    private Res r; 
    Producer(Res r)
    {
        this.r = r;
    }
    public void run()
    {
        while (true)
            r.set("麵包");
    }
}

// 消費者
class Consumer implements Runnable
{
    private Res r;
    Consumer(Res r)
    {
        this.r = r;
    }
    public void run()
    {
        while (true)
            r.get();
    }
}

class ProducerConsumerDemo3
{
    public static void main(String[] args) 
    {
        // 1、建立資源
        Res r = new Res();
        // 2、建立兩個任務。
        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);

        // 3、建立執行緒
        Thread t0 = new Thread(pro);
        Thread t1 = new Thread(pro);
        Thread t2 = new Thread(con);
        Thread t3 = new Thread(con);

        t0.start();
        t1.start();
        t2.start();
        t3.start();
    }
}

修改完之後,我們再次執行以上程式,又會產生一個問題,那就是死鎖了,所有的執行緒都處於凍結狀態。 在這裡插入圖片描述 究其原因是本方執行緒在喚醒時,又一次喚醒了本方執行緒,而本方執行緒迴圈判斷標記,又繼續等待,而導致所有的執行緒都等待了。那又該如何解決這個問題呢?解決手段很明顯,即本方執行緒如果喚醒了對方執行緒,就可以解決了(可以使用notifyAll()方法)。大家可能有疑問,那不是全喚醒了嗎?既有本方執行緒,又有對方執行緒,但是本方執行緒醒後會判斷標記,繼續等待,這樣對方就有執行緒可以執行了。如此一來,多個生產者和消費者案例的程式碼就要修改為:

// 描述資源
class Res
{
    private String name; // 資源名稱
    private int count = 1; // 資源編號

    // 定義標記。
    private boolean flag;
    // 提供了給商品賦值的方法
    public synchronized void set(String name) //
    {
        while (flag) // 判斷標記為true,就執行wait()等待。為false,就生產。
            try{this.wait();}catch(InterruptedException e){ } // t0(等待)、t1(等待)
        this.name = name + "--" + count; // 麵包1、麵包2、麵包3

        count++; // 4

        System.out.println(Thread.currentThread().getName() + "...生產者..." + this.name); // t0、麵包1  t0、麵包2、t1、麵包3
        // 生產完畢,將標記改為true。
        flag = true;

        // 喚醒所有等待執行緒(包括本方執行緒)。
        this.notifyAll();
    }

    // 提供獲取一個商品的方法
    public synchronized void get() // t3
    {
        while (!flag)
            try{this.wait();}catch(InterruptedException e){ } // t2(等待)、t3(等待)
        System.out.println(Thread.currentThread().getName() + ".......消費者......." + this.name); // t2、麵包1

        // 將標記改為false。
        flag = false;
        // 喚醒所有等待執行緒(包括本方執行緒)。
        this.notifyAll();
    }
}

// 生成者
class Producer implements Runnable
{
    private Res r; 
    Producer(Res r)
    {
        this.r = r;
    }
    public void run()
    {
        while (true)
            r.set("麵包");
    }
}

// 消費者
class Consumer implements Runnable
{
    private Res r;
    Consumer(Res r)
    {
        this.r = r;
    }
    public void run()
    {
        while (true)
            r.get();
    }
}

class ProducerConsumerDemo3
{
    public static void main(String[] args) 
    {
        // 1、建立資源
        Res r = new Res();
        // 2、建立兩個任務。
        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);

        // 3、建立執行緒
        Thread t0 = new Thread(pro);
        Thread t1 = new Thread(pro);
        Thread t2 = new Thread(con);
        Thread t3 = new Thread(con);

        t0.start();
        t1.start();
        t2.start();
        t3.start();
    }
}

這樣就已經實現了多生產多消費,但是還有些小問題,那就是效率有點低,因為notifyAll()方法也喚醒了本方執行緒,做了不必要的判斷。為了解決這個效率有點低的問題,JDK1.5版本中提供了多執行緒的升級解決方案。下一講就來為大家講解喲!