Java多线程之生产者消费者

你们既从罪里得了释放,就做了义的奴仆。---罗马书6:18

概念介绍

生产者--消费者模型是多线程运用的经典案例,其设定了这样一个场景,生产者消费者分属两个不同线程,但它们都共同拥有一个数据缓冲区,这个缓冲区用来平衡生产者消费者处理数据不同步的问题。

拿生活中的例子举例,比如我们去餐馆吃饭,餐馆出菜的速度不一致,前来餐馆的消费者也时多时少,为了减少消费者等待上菜的时间,餐馆在消费者还没有点菜的时候提前就先做一部分菜出来,这提前做出来的菜就可以被看做是缓冲区,新的消费者到来时,餐馆直接从缓冲区拿出现成的菜给消费者,这样就减少了消费者等待菜品的时间,同时我们也要注意提前做多少菜是要有一定限制的,做的太多,会导致菜品放置时间过长,食物变质,做的太少又起不到缓冲的作用。

举例说明

BlockingQueue实现

BlockingQueue实现方式应该是最简单易懂的,主要是因为BlockingQueue这个缓冲区已经实现了锁机制,具体来说就是BlockingQueue能定义缓冲区间的大小,同时这个缓冲区能保证向里添加数据和向外提供数据在同一个时刻只能选择其中一个。生产线程只管向里添加数据,缓冲区满了,会自动block添加动作,同样的消费线程也只管从里面取数据,缓冲区空了,也会自动block取的动作,直到缓冲区有了新的数据。
下面放出代码。

package com.azhengye.test;

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

public class ProduConsTest {
    public static void main(String[] args) {
        BlockingQueue queue = new LinkedBlockingQueue<>(10);
        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

class Producer implements Runnable {
    BlockingQueue bufferQueue;
    int i = 0;

    Producer(BlockingQueue shareQueue) {
        this.bufferQueue = shareQueue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Random random = new Random();
                int i = random.nextInt();
                //System.out.println("生产者开始生产了");
                bufferQueue.put(i);
                System.out.println("生产了" + i);
            } catch (InterruptedException e1) {
                i--;
                e1.printStackTrace();
            }
        }

    }
}

class Consumer implements Runnable {
    BlockingQueue shareQueue;

    Consumer(BlockingQueue shareQueue) {
        this.shareQueue = shareQueue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                //System.out.println("消费者来了");
                int i = shareQueue.take();
                System.out.println("消费了" + i);
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        }

    }
}

运行结果如下图

Java多线程之生产者消费者_第1张图片
这里写图片描述

这里似乎出现了问题,一开始就出现了 消费了1405535437的log,还没有生产呢,消费的数据从哪来的。太不合理了。

别急我们打开注释掉的语句在看看结果


Java多线程之生产者消费者_第2张图片
这里写图片描述

配合着打开注释的输出结果,我们体会下为什么出现了第一次那样不合理的结果。

由于Producer跟Consumer分属两个不同的线程,同时启动它俩,系统就会在它们直接来回切换。当Producer线程刚执行完 bufferQueue.put(i); 语句后,恰好系统切换至Consumer线程,此时shareQueue里已经有了数据,于是执行了

int i = shareQueue.take();
System.out.println("消费了" + i);

从而我们就首先看到消费了1405535437的怪异log。从这点我们也可以看到多线程执行顺序的不确定性。

wait()-notify()实现

wait,notifiy以及notifyAll方法都是Object中的方法,要搞清它们的用法,我们必须要弄清楚2个基本原则:

  • wait和notifiy/notifyAll都要在synchronized代码块内部调用
  • wait和notifiy/notifyAll都被同一个对象调用才有意义。

现在来模拟实现一个模型:一个生产者不断向队列插入数据,多个消费者从队列中删除数据。

package com.azhengye.test;

import java.util.LinkedList;
import java.util.Random;

public class ProduConsTestNotifyWaitDemo {
    private static final int BUFF_SIZE = 10;
    private static final int CONSUMER_COUNT = 5;

    public static void main(String[] args) {
        LinkedList buffList = new LinkedList();
        Producer producer = new Producer(buffList);
        Consumer consumer = new Consumer(buffList);
        for (int i = 0; i < CONSUMER_COUNT; i++) {
            new Thread(consumer).start();
        }
        new Thread(producer).start();
    }
    
    static class Producer implements Runnable {
        private LinkedList list = null;

        public Producer(LinkedList list) {
            this.list = list;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (list) {
                    if (list.size() > BUFF_SIZE) {// 用if判断是个坑
                        try {
                            System.out.println("Producer:"
                                    + Thread.currentThread().getId() + "wait");
                            list.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    Random random = new Random();
                    int i = random.nextInt();
                    System.out.println("Producer:"
                            + Thread.currentThread().getId() + "增加了内容为" + i
                            + "的节点");
                    list.add(i);
                    list.notifyAll();
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static class Consumer implements Runnable {
        private LinkedList list = null;

        public Consumer(LinkedList list) {
            this.list = list;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (list) {
                    if (list.isEmpty()) {// 用if判断是个坑
                        System.out.println("Consumer"
                                + Thread.currentThread().getId() + "wait");
                        try {
                            list.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Consumer:"
                            + Thread.currentThread().getId() + "移除了内容为"
                            + list.remove() + "的节点");
                    list.notifyAll();
                }
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果如下图


Java多线程之生产者消费者_第3张图片
异常结果

代码注释里已经标记了埋坑点,来仔细分析下这个错误过程:

  1. Consumer8~Consumer12因list未空,没什么要"消费"的,因此都处于wait状态,逻辑合理。
  2. Producer3向list里添加了一个数据,然后调用notifyAll通知1步wait状态的Consumer8~Consumer12别等了,开始等着系统派活了,但系统只会从Consumer8~Consumer12随机抽取一个来干活,Consumer12被抽中了,于是它去干活,Consumer8~Consumer11继续去wait,Consumer12删除了list里唯一的数据,然后也调用notifyAll让处于wait状态的别等了,系统又要重新派活了。到此逻辑也合理。
  3. Consumer8~Consumer11其中的某一个被系统抽中了,假如Consumer8被抽中,由于判断条件为if,Consumer8跳出if语句继续往下执行,注意2步已经使得list为空了,因此Consumer8执行到list.remove() 出现了上图的结果,异常出现了。

为了避免上述错误,在添加一个原则

  • 在while循环里判断操作条件,而不是if。

将if换成while后程序运行正常:


Java多线程之生产者消费者_第4张图片
正常运行结果

额外说明:
提下wait跟sleep的区别。表面看起来它们都是停止了当前线程的工作任务,但本质却不同。wait是需要别人去notify才会继续工作,而sleep(n)在n毫秒后自己就醒来了,不用去依赖别人。
synchronized锁住的是对象,因此针对该对象的synchronized方法同一时间只能有一个线程去访问

你可能感兴趣的:(Java多线程之生产者消费者)