[05]从零开始的JAVAEE-阻塞队列及模拟实现

目录

生产者消费者模型

生产者消费者模型的初心

耦合

内聚

解耦合

 削峰填谷

阻塞队列的使用

代码模拟实现阻塞队列

实现一个普通队列

加上线程安全

加上阻塞功能


生产者消费者模型

为了更好的理解阻塞队列的使用常见,首先来理解这个生产者消费者模型。

假设现在是过年期间,大家其乐融融,围在一起包饺子

 假设四个滑小稽用这种模式来包饺子:自己擀一个面皮-包一个饺子。

这样虽然可以完成包饺子的任务,但并不高效,并且只有一个擀面杖,四个滑小稽还需要对这个擀面杖进行竞争。为了提高效率,他们换了一种包饺子策略

一个滑小稽负责擀面皮,三个滑小稽负责包

 这样就构成了一个最简单的生产者-消费者模型

 生产者负责生产资源,消费者负责消耗资源,(这里的资源指的就是饺子皮)而两者需要一个交易场所进行交互资源,这里的交易场所就是桌子

而这里桌子又具有这样的特性

  • 如果负责擀面皮的滑小稽的速度>三个负责包饺子的滑小稽的速度:桌子上一直有面皮,负责包饺子的滑小稽可以一直包饺子
  • 如果负责擀面皮的滑小稽的速度<三个负责包饺子的滑小稽的速度:桌子每放上一个面皮就会迅速被三个负责包饺子的滑小稽消耗掉,一旦桌子上没有面皮,三个负责包饺子的滑小稽就会陷入阻塞等待的状态。

而在JAVA中,这里的桌子就被称为:阻塞队列(BlockingDeque)

生产者消费者模型的初心

生产者消费者模型的初心主要是为了解决两个方面

  1. 让上下游模块间进行更好的“解耦合”
  2. 削峰填谷

耦合

指的是两个模块之间的关联性关系,关联越强,耦合性越高,反之越低

例如你的亲人生病了,你就要放下手头的工作去陪他,这对你原来对自己的工作生活安排是有较大影响的,这就是高耦合

如果是你并不熟的同学/同事生病了,你就只需要在微信上表达一下关心,不需要调整原来的生活工作安排,这就是低耦合

内聚

内聚是指模块内各元素(比如函数、类等)之间彼此联系紧密,共同完成某一个功能或者单一目的的度量

例如你和你的女朋友同居了,你的生活习惯很不好,对于穿过的衣服,随手乱丢,到处摆放,而你的女朋友对于穿过的衣服则是耐心整理,归类存放,这样在日后你需要找一件衣服时,你就会遍历整个屋子去寻找,这就是低内聚。而你的女朋友找一件衣服时,只需要遍历她分类的那个衣柜即可。这就是高内聚

解耦合

我们来考虑这样一个场景:有A B两个服务器

 此时A和B直接交换数据,就可以说A、B是高耦合的,因为如果A挂了就会直接影响B,而B挂了也会直接影响A,而且如果此时我们想要加入一个新的服务器C,还会涉及到比较麻烦的调整。 

为了解决这个问题,我们就可以使用阻塞队列来解耦合

 此时就用到了生产者-消费者模型,A是生产者,B是消费者,此时A和B都不知道彼此的存在,只通过一个阻塞队列来进行数据交换。

而此时在加入服务器C只需要让C从队列中取元素即可。

 削峰填谷

同样是A和B两个服务器直接关联,A负责接收请求,B负责处理请求。用户行为会影响A,而用户行为是随机的情况,有些情况下A就会出现一波“峰值”,爆发性增长一波。

服务器的处理资源能力都是有限的,因为服务器处理每个请求都需要消耗硬件资源,很有可能A受到过多的请求导致硬件资源达到瓶颈,进而导致B挂掉了。

为了解决这个问题,就可以使用这样的模型

还是使用阻塞队列来关联A-B,A收到的请求多了,队列中的元素也就多了,但是B仍然可以按照固定的速率来处理请求,这样队列就帮B承担了压力,这就叫“削峰”。

峰值过后,A不在接收大量数据,此时数据积压在队列中,B仍然可以在非高峰期以稳定的速率来处理请求,这就叫做“填谷”。

当然队列也有挂的风险,但队列是一个相对稳定的代码,不像A、B,要频繁更改需求,更改代码,队列挂掉的风险远远小于A、B。

阻塞队列的使用

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;


public class work2 {

    public static void main(String[] args) throws InterruptedException{
        BlockingDeque<Integer> queue = new LinkedBlockingDeque<>();//双端阻塞队列

        //消费者 从队列中取元素
        Thread t1 = new Thread(()->{
            while (true){
                try {
                    int val = queue.take();
                    System.out.println("消费元素:"+val);
                    System.out.println();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        //生产者,放元素到队列中
        Thread t2 = new Thread(()->{
            int i = 0;
            while (true){
                try {
                    int val = i;
                    i++;
                    queue.put(val);
                    System.out.println("生产元素:"+val);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

这就是一个生产者-消费者模型的阻塞队列代码案例。

import java.util.concurrent;
BlockingDeque<Integer> queue = new LinkedBlockingDeque<>();//双端阻塞队列

这是java中自带的阻塞队列,BlockingDeque,它是一个接口,LinkedBlockingDeque是一个基于链表实现的双端阻塞队列,它实现了BlockingDeque这个接口,在java.util.concurrent这个包中。

  • take():是阻塞队列中取元素的方法
  • put():是阻塞队列中入元素的方法

这里的t1、t2分别代表了消费者-生产者,其中t2一直在生产元素,速率为每秒生产一个,t1一直在消费元素,速率不做限制,运行结果为

 可以看到,t2每生产一个元素t1会立马消耗掉,如果队列中没有元素,t1会进行阻塞等待。

代码模拟实现阻塞队列

实现一个阻塞队列,我们可以分为三步

  • 1.实现一个普通队列
  • 2.加上线程安全
  • 3.加上阻塞功能

实现一个普通队列

我们这里使用数组来实现一个循环队列,一个循环队列应该有这样一些属性

  • 一个数组用于存放元素
  • head变量用于记录队列头
  • tail变量用于记录队列尾

并且有这样一些方法

  • put()入队列
  • take()出队列

还有这样一些特性

  • 先进先出,后进后出
  • 当tail走到数组尾时,如果队列不为满,将tail放到数组首地址,循环利用空间

 由于队列为空和队列为满时,tail和head都是重合的,那么如何判断这两个条件呢

  • 浪费一个空间,即当(tail +1) % arr.length == head)时判定为满,head == tail时判定为空
  • 获取队列元素个数,即添加一个变量来记录队列元素个数,当队列元素等于数组长度时说明队列满

下面来写代码

class MyBlockingQueue{
    public int[] nums = new int[100];
    public int head = 0;
    public int tail = 0;
    public int size = 0;

    //插入
    public void put(int val){
        //判定是不是满
        if(size == nums.length){
            //满了
            System.out.println("队列满");
            return;
        }
        nums[tail] = val;
        tail++;
        //如果tail到尾,让tail从头
        if(tail == nums.length){
            tail  = 0;
        }
        size++;
    }
    //出队列
    public Integer take(){
        //判定是否为空
        if(tail == head){
            System.out.println("队列空");
            return null;
        }
        int val = nums[head];
        head++;
        if(head == nums.length){
            head = 0;
        }
        size--;
        return val;
    }
}

加上线程安全

在上面的代码中,涉及到各种读、写操作,在前面的文章中介绍过,多线程中由于线程随机调度,多个线程同时修改一个变量时会出现问题,多个线程读一个变量时也会出现问题,解决办法就是加锁和volatile关键字

一个很简单粗暴的办法,把所有涉及到写操作的方法加上synchronized,所有涉及到读操作的变量加上volatile。

class MyBlockingQueue{
    public int[] nums = new int[100];
    volatile public int head = 0;
    volatile public int tail = 0;
    volatile public int size = 0;

    //插入
    synchronized public void put(int val){
        //判定是不是满
        if(size == nums.length){
            //满了
            System.out.println("队列满");
            return;
        }
        nums[tail] = val;
        tail++;
        //如果tail到尾,让tail从头
        if(tail == nums.length){
            tail  = 0;
        }
        size++;
    }
    //出队列
    synchronized public Integer take(){
        //判定是否为空
        if(tail == head){
            System.out.println("队列空");
            return null;
        }
        int val = nums[head];
        head++;
        if(head == nums.length){
            head = 0;
        }
        size--;
        return val;
    }
}

加上阻塞功能

阻塞功能是这个队列的灵魂所在,现在我们给它加上

需要加上的功能有

  • 队列满,队列阻塞,当有元素出队列时,唤醒队列
  • 队列空,队列阻塞,当有元素插入队列时,唤醒队列

修改后的代码如下

class MyBlockingQueue{
    public int[] nums = new int[100];
    volatile public int head = 0;
    volatile public int tail = 0;
    volatile public int size = 0;

    //插入
    synchronized public void put(int val)throws InterruptedException{
        //判定是不是满
        while(size == nums.length){
            //满了
            System.out.println("队列满");
            this.wait();//因为这里的锁对象就是this
        }
        nums[tail] = val;
        tail++;
        //如果tail到尾,让tail从头
        if(tail == nums.length){
            tail  = 0;
        }
        size++;
        this.notify();
    }
    //出队列
    synchronized public Integer take() throws InterruptedException{
        //判定是否为空
        while(size == 0){
            System.out.println("队列空");
            this.wait();//因为这里的锁对象就是this
        }
        int val = nums[head];
        head++;
        if(head == nums.length){
            head = 0;
        }
        size--;
        this.notify();//队列从满到不满,唤醒队列
        return val;
    }
}

 这是一个相互唤醒的过程。

注意:在这个图中,如果使用if来判断,wait调用后,保不齐在别的线程中调用一个interrupt方法就直接唤醒线程了,所以应该使用while循环判断

本文到这里就结束了,下文将介绍多线程开发中常用到的定时器模型和代码的模拟实现,下期见

  • 13
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不卷啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值