JavaEE初阶 - 多线程基础篇(生产者 - 消费者模型)

阻塞队列
生产者 - 消费者模型
标准库中的阻塞队列
循环队列实现阻塞队列
实现生产者 - 消费者模型


阻塞队列

阻塞队列是一个符合先进先出的队列, 相比于普通队列, 阻塞队列有一些其他方面的功能:

  1. 线程安全
  2. 产生阻塞效果
    1). 如果队列为空, 此时尝试出队列, 就会出现阻塞, 直到队列不为空为止
    2). 如果队列为满, 此时尝试入队列, 也会出现阻塞, 直到队列不为满为止

基于上述特性, 就可以实现"生产者-消费者模型"(这是日常开发中, 处理多线程问题的一个典型的方式)

生产者 - 消费者模型

有两个服务器A, B, 服务器A作为入口服务器, 用来接收用户发送的数据, 服务器B作为应用服务器, 来为A提供一些数据.
JavaEE初阶 - 多线程基础篇(生产者 - 消费者模型)_第1张图片
  如果不使用生产者 - 消费者模型, 此时A和B的耦合性较强, 一旦服务器B出现故障, 很有可能导致服务器A也出现故障.

而如果使用生产者 - 消费者模型, 就可以降低A和B的耦合度.
JavaEE初阶 - 多线程基础篇(生产者 - 消费者模型)_第2张图片

  为服务器A和B之间添加一条阻塞队列, 这样, A和B之间就不需要进行交互, 一旦一个服务器出现问题, 另一个也不会受到影响.

这就是生产者 - 消费者模型的优点一:能够让多个服务器程序之间更充分地解耦合.

  对于这两个服务器, 如果某一时刻请求量暴涨(不可控), 此时A作为入口服务器, 并不需要进行过多的计算, 并不会受到太多的影响, 但B作为应用服务器, 需要进行大量的计算, 也需要更多的系统资源, 此时如果硬件资源不够, 很可能会导致服务器崩溃.

  此时如果采用生产者 - 消费者模型, A的请求暴涨只会导致阻塞队列的请求量暴涨, 而阻塞队列只需要存储数据, 阻塞队列只需要继续按照原有的速度对B服务器发送请求, B也就不会收到影响了.
JavaEE初阶 - 多线程基础篇(生产者 - 消费者模型)_第3张图片
这就是生产者 - 消费者模型的优点二:能够对于请求进行"削峰填谷".

  这里的阻塞队列的功能仅限于存储数据, 然而, 实际开发中用到的并不是阻塞队列, 而是消息队列, 消息队列会实现更多的功能, 例如, 数据持久化存储, 支持多个数据通道, 支持多节点容灾冗余备份, 支持管理面板, 方便配置参数…消息队列并不是这里的一个简单的数据结构, 而是一组服务器程序.

标准库中的阻塞队列

Java标准库中内置了阻塞队列, 当我们需要在程序中实现阻塞队列时, 直接调用即可.

  1. BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
  2. put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
  3. BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性
public static void main(String[] args)throws InterruptedException {
        //生成阻塞队列
        BlockingQueue<String > queue = new LinkedBlockingQueue<>();
        //入队列
        queue.put("hello");
        //出队列
        System.out.println(queue.take());
}
//结果:
hello

实现阻塞队列

先创建一个循环队列:

class MyBlockingQueue{
    //数组实现循环队列
    static int[] array = new int[100];
    //队首
    private int head;
    //队尾
    private int tail;
    //队列长度
    private int size;

    //入队列
    public void put(int value){
        if(size == array.length){
            return;
        }
        array[tail] = value;
        ++size;
        if(++tail >= array.length){
            tail = 0;
        }

    }

    //出队列
    public Integer take(){
        if(size == 0){
            return null;
        }
        int ret = array[head];
        ++head;
        if(head >= array.length){
            head = 0;
        }
        --size;
        return ret;
    }
}

  创建好循环队列之后, 就要让这个队列支持线程安全, 保证在多线程下, 调用这里的put()和take()方法不出现问题, 那么就要对方法进行加锁.

  可以看到, 这两个方法中到处都在对公共变量进行修改, 那我们直接对方法进行加锁即可:

//入队列
synchronized public void put(int value){
    
}

//出队列
synchronized public Integer take(){
    
}

  保证了线程安全之后, 接下来就只需要实现阻塞效果了. 我们想要实现的阻塞效果是:队列为满时入队列, 产生阻塞, 队列不满时, 阻塞结束; 队列为空时出队列, 产生阻塞, 队列不为空时, 阻塞结束.

线程安全的阻塞队列的最终实现:

class MyBlockingQueue{
    //数组实现循环队列
    static int[] array = new int[100];
    //队首
    private int head;
    //队尾
    private int tail;
    //队列长度
    private int size;

    //入队列
    synchronized public void put(int value) throws InterruptedException {
        if(size == array.length){
            //如果队列满时入队列, 使用wait()产生阻塞
            this.wait();
        }
        array[tail] = value;
        ++size;
        if(++tail >= array.length){
            tail = 0;
        }
        //这里元素入队列成功, 唤醒take()中的等待
        this.notify();
    }

    //出队列
    synchronized public Integer take() throws InterruptedException{
        if(size == 0){
            //如果队列为空时出队列, 使用wait()产生阻塞
            this.wait();
        }
        int ret = array[head];
        ++head;
        if(head >= array.length){
            head = 0;
        }
        --size;
        //这里元素出队列成功, 唤醒put()中的等待
        this.notify();
        return ret;
    }
}

实现生产者 - 消费者模型

public class Demo {
    static MyBlockingQueue queue = new MyBlockingQueue();
    public static void main(String[] args) {
        //创建生产者
        Thread producer = new Thread(() ->{
            //随机生成入队元素
            Random random = new Random();
            int num = 0;

            //生产者中每次入队一个元素
            while (true){
                num = random.nextInt(101);
                try {
                    queue.put(num);
                    System.out.println("生产者: 生产了"+num);

                    //当生产者生产速度低时, 消费者的消费速度也会降低
                    //Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //消费者
        Thread consumer = new Thread(()->{
            while (true){
                try {
                    int num = queue.take();
                    System.out.println("消费者: 消费了"+num);
                    //当生产速度远大于消费速度时, 如果造成阻塞队列满, 此时消费者每消费一次, 生产者才会生产
                    //Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
        consumer.start();
    }
}

  上述代码实现了一个最简单的生产者 - 消费者模型, 元素入队列一次表示一次生产, 元素出队列一次表示一次消费; 生产者和消费者中的Thread.sleep(1000)分别代表了生产速度慢和消费速度慢的情况, 当生产速度较慢时, 消费速度会随之降低; 当生产速度过快时, 一旦阻塞队列满了, 生产者就不会继续生产, 直到阻塞队列不为空.

你可能感兴趣的:(JavaEE初阶,java-ee,学习,java)