java多線程之生產者消費者問題

場景

生產者生產商品存放在工廠中,消費者從工廠中取出商品。工廠最多隻能存放一件商品。

一個生產者一個消費者

public class ProductFactory {

    private List<String> goods = new ArrayList<>();

    private static int index;

    synchronized public void add(){
        try{
            while(goods.size() == 1){
                this.wait();
            }
            goods.add("商品" + ++index);
            this.notify();
            System.out.println("add" + goods.get(0));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public String del(){
        try{
            if(goods.size() == 0){
                this.wait();
            }
            value = goods.get(0);
            goods.remove(0);
            this.notify();
            System.out.println("del" + value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return value;
    }
}

這裏使用List對象來存放數據, produce方法和consume方法使用synchronized進行同步。

public class Producer {

    private ProductFactory factory;
    public Producer(ProductFactory factory){
        this.factory = factory;
    }

    public void produce(){
        factory.add();
    }

}

public class Consumer {

    private ProductFactory factory;

    public Consumer(ProductFactory factory){
        this.factory = factory;
    }

    public void consume(){
        factory.del();
    }
}

生產者和消費者的線程代碼

class ThreadP extends Thread{
    private Producer producer;

    public ThreadP(Producer producer){
        this.producer = producer;
    }

    @Override
    public void run() {
        while(true)
            producer.produce();
    }
}

class ThreadC extends Thread{
    private Consumer consumer;

    public ThreadC(Consumer consumer){
        this.consumer = consumer;
    }

    @Override
    public void run() {
        while(true)
            consumer.consume();
    }
}

運行類的代碼

public class TestProducerConsumer {

    public static void main(String[] args) {

        ProductFactory factory = new ProductFactory();
        Producer producer = new Producer(factory);
        ThreadP threadP = new ThreadP(producer);

        Consumer consumer = new Consumer(factory);
        ThreadC threadC = new ThreadC(consumer);
        threadP.start();
        threadC.start();

        return;
    }
}

程序的輸出:

...
add商品24265
del商品24265
add商品24266
del商品24266
add商品24267
del商品24267
...

可以看到程序的結果是生產者先生產一個商品,然後消費者消費一個商品,也就是生產者和消費者在交替的運行。

一個生產者多個消費者

我們修改運行類的代碼

public class TestProducerConsumer {

    public static void main(String[] args) {

        ProductFactory factory = new ProductFactory();
        Producer producer = new Producer(factory);

        ThreadP threadP = new ThreadP(producer);

        Consumer consumer = new Consumer(factory);
        Consumer consumer1 = new Consumer(factory);
        Consumer consumer2 = new Consumer(factory);
        Consumer consumer3 = new Consumer(factory);
        ThreadC threadC = new ThreadC(consumer);
        ThreadC threadC1 = new ThreadC(consumer1);
        ThreadC threadC2 = new ThreadC(consumer2);
        ThreadC threadC3 = new ThreadC(consumer3);

        threadP.start();
        threadC.start();
        threadC1.start();
        threadC2.start();
        threadC3.start();

        return;
    }
}

程序的輸出

add商品1
Exception in thread "Thread-1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
del商品1
    at java.util.ArrayList.rangeCheck(ArrayList.java:657)
add商品2
del商品2
    at java.util.ArrayList.get(ArrayList.java:433)
    at multithread.producerconsumer.ProductFactory.del(ProductFactory.java:39)
    at multithread.producerconsumer.Consumer.consume(Consumer.java:18)
    at multithread.producerconsumer.ThreadC.run(TestProducerConsumer.java:105)

我們分析爲什麼會發生消費者數組越界的異常,也就是爲什麼在判斷了goods.size() == 0之後,消費者線程調用了wait方法,線程被掛起了之後,消費者線程還執行了goods.remove(0)

假設目前商品的數量爲1,消費者線程將會執行

value = goods.get(0);
goods.remove(0);
this.notify();
System.out.println("del" + value);

當前消費者線程首先刪除goods中僅有的一個元素,此時goods已經爲空。緊接着調用notify()方法,該消費者線程會在執行完該同步方法剩下的語句之後,釋放該對象鎖。等待該對象鎖的線程出了生產者線程,還有其他幾個消費者線程。由於notify會隨機讓一個等待對象鎖的線程執行,所以很有可能被選中的是一個消費者線程。
如果選中的是另一個消費者線程,那麼會從該消費者線程wait()方法接着往下執行。

if(goods.size() == 0){
    this.wait();
}
value = goods.get(0);
goods.remove(0);
this.notify();
System.out.println("del" + value);

也就是離開if語句,再次執行了goods.remove(0),所以發生了數組越界。

這裏發生問題的地方就是一個消費者線程喚醒了另一個消費者線程,而對goods大小的判斷卻沒有起到作用。所以要修改這個問題,可以把消費者線程判斷goods大小的語句if替換成while。

public class ProductFactory {

    private List<String> goods = new ArrayList<>();

    private static int index;

    synchronized public void add(){
        try{
            if(goods.size() == 1){
                this.wait();
            }
            goods.add("商品" + ++index);
            this.notify();
//            this.notifyAll();
            System.out.println("add" + goods.get(0));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public String del(){
        String value = "";
        try{
            while(goods.size() == 0){
                this.wait();
            }

            value = goods.get(0);
            goods.remove(0);
            this.notify();
            System.out.println("del" + value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return value;
    }
}

修改後運行代碼,結果是

add商品1
del商品1
add商品2
del商品2

這次程序雖然沒有出現數組越界異常,但是程序卻出現了假死。
我們將if替換成while,雖然可以避免發生異常,但是消費者喚醒消費者的現象卻依然存在。程序出現假死的主要原因也就是連續喚醒同類。那麼只要不光喚醒同類線程,也將異類線程一起喚醒就可以解決了,也就是利用notifyAll()方法。

public class ProductFactory {

    private List<String> goods = new ArrayList<>();

    private static int index;

    synchronized public void add(){
        try{
            if(goods.size() == 1){
                this.wait();
            }
            goods.add("商品" + ++index);
//            this.notify();
            this.notifyAll();
            System.out.println("add" + goods.get(0));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public String del(){
        String value = "";
        try{
            while(goods.size() == 0){
                this.wait();
            }

            value = goods.get(0);
            goods.remove(0);
//            this.notify();
            this.notifyAll();
            System.out.println("del" + value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return value;
    }
}

將兩個同步方法中的notify方法替換成notifyAll方法,運行程序

...
add商品49834
del商品49834
add商品49835
del商品49835
add商品49836
del商品49836
...

可以看到程序正常運行了。

多個生產者一個消費者

我們讓ProductFactory的代碼回到開始的轉態,也就是沒有使用while和notifyAll的狀態,

public class ProductFactory {

    private List<String> goods = new ArrayList<>();

    private static int index;

    synchronized public void add(){
        try{
            if(goods.size() == 1){
                this.wait();
            }
            goods.add("商品" + ++index);
            this.notify();
//            this.notifyAll();
            System.out.println("add" + goods.get(0));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public String del(){
        String value = "";
        try{
            if(goods.size() == 0){
                this.wait();
            }

            value = goods.get(0);
            goods.remove(0);
            this.notify();
//            this.notifyAll();
            System.out.println("del" + value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return value;
    }
}

把運行類的代碼修改爲多個生產者和一個消費者

public class TestProducerConsumer {

    public static void main(String[] args) {

        ProductFactory factory = new ProductFactory();
        Producer producer = new Producer(factory);
        ThreadP threadP = new ThreadP(producer);

        Consumer consumer = new Consumer(factory);

        ThreadC threadC = new ThreadC(consumer);     

        Producer producer1 = new Producer(factory);
        Producer producer2 = new Producer(factory);
        Producer producer3 = new Producer(factory);
        ThreadP threadP1 = new ThreadP(producer1);
        ThreadP threadP2 = new ThreadP(producer2);
        ThreadP threadP3 = new ThreadP(producer3);
        threadP.start();
        threadP1.start();
        threadP2.start();
        threadP3.start();

        threadC.start();
        return;
    }
}

運行結果:

...
del商品30718
del商品30719
del商品30720
add商品30721
add商品30721
add商品30721
add商品30721
...

從程序的輸出我們看到生產者和消費者線程不是交替執行的。按照我們的設想應該是生產者線程生產一個商品,然後消費者線程將這個商品消費掉。
出現這個問題的原因,和上面一個生產者多個消費者問題的原因是一樣的。由於在同步方法中使用的是if判斷goods的大小,生產者可能會被生產者喚醒,喚醒後接着執行if後面的語句。解決這個問題將if緩存while就可以了。
僅僅將if緩存while同樣會遇到程序假死的問題,問題的原因上面已經解釋過了,還是要把notify方法替換成notifyAll方法。

多個生產者多個消費者

根據上面的分析,多個生產者和多個消費者的代碼就很容易了,只需要注意:
1. 同步方法中使用while而不是if來判斷條件;
2. 使用notifyAll方法喚醒所以的線程。

public class ProductFactory {

    private List<String> goods = new ArrayList<>();

    private static volatile int index;

    synchronized public void add(){
        try{
            while(goods.size() == 1){
                this.wait();
            }
            goods.add("商品" + ++index);
//            this.notify();
            this.notifyAll();
            System.out.println("add" + goods.get(0));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    synchronized public String del(){
        String value = "";
        try{
            while(goods.size() == 0){
                this.wait();
            }

            value = goods.get(0);
            goods.remove(0);
//            this.notify();
            this.notifyAll();
            System.out.println("del" + value);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return value;
    }
}

運行類

public class TestProducerConsumer 
    public static void main(String[] args) {
        ProductFactory factory = new ProductFactory();
        Producer producer = new Producer(factory);
        ThreadP threadP = new ThreadP(producer);

        Consumer consumer = new Consumer(factory);
        Consumer consumer1 = new Consumer(factory);
        Consumer consumer2 = new Consumer(factory);
        Consumer consumer3 = new Consumer(factory);
        ThreadC threadC = new ThreadC(consumer);
        ThreadC threadC1 = new ThreadC(consumer1);
        ThreadC threadC2 = new ThreadC(consumer2);
        ThreadC threadC3 = new ThreadC(consumer3);

        Producer producer1 = new Producer(factory);
        Producer producer2 = new Producer(factory);
        Producer producer3 = new Producer(factory);
        ThreadP threadP1 = new ThreadP(producer1);
        ThreadP threadP2 = new ThreadP(producer2);
        ThreadP threadP3 = new ThreadP(producer3);

        threadC.start();
        threadC1.start();
        threadC2.start();
        threadC3.start();

        threadP.start();
        threadP1.start();
        threadP2.start();
        threadP3.start();
        return;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章