阻塞队列与消费者生产者模型

一.什么是阻塞队列

阻塞队列是在普通的先进先出队列的基础上进行了扩充

  • 线程安全
  • 具有阻塞性

         a)如果队列没空进行出队列操作,此时就会出现阻塞;一直阻塞到其他线程往队列添加元素

         b)如果队列为满进行入列操作,此时也会出现阻塞;一直阻塞到其他线程从队列取走元素

基于阻塞队列,最大的应用场景就是实现“生产者消费者模型”。

二.生产者消费者模型

1)生产者消费者模型是什么

以包饺子为例

假设有三个人A,B,C在桌前包饺子,包饺子有擀饺子皮包饺子。

第一种典型包法,三个人,分别进行擀饺子皮和包饺子这两操作。

这种方法三个人需竞争擀面杖,涉及阻塞等待,较为低效。

第二种典型包法,三人分工合作

A负责擀饺子皮,BC负责包饺子。

此时,A是饺子皮的生产者,BC是饺子皮的消费者。

而用于存放擀好的饺子皮的桌子就是阻塞队列 

生产者与消费者模型在后端发开中会经常涉及,当下后端开发,常用“分布式结构”,不是一台服务器解决所有问题,而是分成多个服务器,服务器之间互相调用。

通常谈到的“阻塞队列”是代码中的一个数据结构。

但由于这东西太好用了,以至于会把这样的数据结构单独封装成一个服务器程序。

并且在单独的服务器上进行部署

这样的阻塞队列有了一个新的名字“消息队列”(Message Queue,MQ

2)生产者消费者模式的优点

使用生产者消费者模型有两方面好处:

  1)服务器之间的“解耦合”(模块之间的关联程度)

如果是直接调用的关系

编写A代码中,就会出现许多B服务器相关的代码。

编写B代码中,也会出现许多A服务器相关代码。

如果A服务器挂了,可能B服务器也会受到影响,并且,若是后续想增加一个C服务器,对于A的改动就会较大。

2)通过中间的阻塞队列,可以起到"削峰填谷"效果。(在遇到请求量激增突发的情况下,可以有效保护下游服务器不会被冲垮)

如果是直接调用,A收到多少请求,B也同样收到多少请求,很有可能把B搞挂。


额外小知识科普:

1)服务器为什么在请求激增的情况下可能会挂?

一台服务器就是一台电脑,上面提供着一些硬件资源(cpu,内存,宽带......)

再好的电脑,硬件资源是有限的,服务器每收到一个请求,处理请求时都要执行一些代码,需消耗一定的硬件资源。

当这些请求消耗的总硬件资源超过了机器提供的上限,那机器就会卡死,程序崩溃,服务器就挂了

2)在请求激增的时候,为啥A服务器不会挂而B服务器更容易挂

A服务器是一个“网关服务器”,工作是收到请求,并转发给其他服务器,消耗的硬件资源更少。

在同样配置下,消耗的硬件资源更少,就能处理更多请求,所以不会那么容易挂。

B服务器是真正干活的服务器,需要真正执行一系列逻辑,消耗的资源更多,在请求激增的情况下更容易挂。

类似的,Mysql这样的数据库,处理每个请求时,做个工作是比较多的,消耗的硬件资源也多,因此Mysql也是系统中容易挂的部分。


3)生产者消费者模式的代价

1.需要更多的机器来部署这些队列。(代价小,小机器不怎么值钱)

2.A和B服务器之间通信的时延会更长。

如果A和B之间的调用要求的响应时间短(银行取钱),就不太适合了。

三.BlockingQueue

阻塞队列在Java标准库中提供现成封装——BlockingQueue。

1.实现

 BlockingQueue<String> queue=new ArrayBlockingQueue<>(100);//这个参数是阻塞队列的容量

BlockingQueue是一个接口,不能直接去new,应该去new实现这个接口的类。

2.使用

Queue提供的一些操作,在BlockingQueue中同样能使用(pull,offer,但用的少)

BlockingQueue提供了两个专属方法

1)put入队列

 queue.put();
public class Dem1 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue=new ArrayBlockingQueue<>(3);//这个参数是阻塞队列的容量
        queue.put("111");
        System.out.println("put成功");
        queue.put("111");
        System.out.println("put成功");
        queue.put("111");
        System.out.println("put成功");
        queue.put("111");
        System.out.println("put成功");
    }
}

代码中4次put,却只有3次成功,这是因为阻塞队列设置的容量为3,第四个put阻塞了。

在jconsole上,也能查看到当前线程处于WAITING状态

2)take出队列

queue.take();
public class Dem1 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue=new ArrayBlockingQueue<>(3);//这个参数是阻塞队列的容量
        queue.put("111");
        System.out.println("put成功");
        queue.put("111");
        System.out.println("put成功");
        queue.take();
        System.out.println("take成功");
        queue.take();
        System.out.println("take成功");
        queue.take();
        System.out.println("take成功");

    }
}

当take()次数多于阻塞队列中元素的值时,也会发生阻塞。

3.生产者消费者模型代码

public class Demo2 {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue=new ArrayBlockingQueue<>(1000); //设置容量为1000
        Thread t1=new Thread(()->{
            int i=0;
            //生产者线程
            while(true){
                try {
                    queue.put(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("成功生产元素"+i);
                i++;
            }
        });
        Thread t2=new Thread(()->{
            //消费者线程
           while (true){
               try {
                  int i= queue.take();
                   System.out.println("消费元素"+i);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        t1.start();
        t2.start();
    }
}

若想要生产慢一些,可通过sleep控制

Thread t1=new Thread(()->{
            int i=0;
            //生产者线程
            while(true){
                try {
                    queue.put(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("成功生产元素"+i);
                i++;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

可以发现,生产者线程慢时,生产一个,消费一个。

上述程序中,一个线程生产,一个线程消费。

在实际开发中,通常多个线程生产,多个线程消费。

四.自己实现一个阻塞队列

1.思路

利用数组实现阻塞队列

head表示头,tail表示尾。当数据put()进队列时,tail向后面移。

若是take()出队列,则将head往后移。

当tail移动到数组末尾时,若队列未满,则将tail移回初始的位置。

若队列已满,则可以浪费最后一个位置,tail指向最后的位置。

2.代码实现

首先先不考虑阻塞与线程安全,代码如下:

class MyBlockingQueue{
    private String[] data=null;
    public MyBlockingQueue(int capacity){
        data=new String[capacity];
    }
    private int head=0;
    private int tail=0;
    private int size=0;

    public  void put(String s){
        if(size==data.length){
            return ;
        }
        data[tail]=s;
        tail++;
        if(tail>data.length){
            tail=0;;
        }
        size++;
    }

    public String take(){
        String ret="";
        if(size==0){
            return null;
        }
        ret=data[head];
        head++;
        if(head>data.length){
            head=0;
        }
        size--;
        return ret;
    }

}

考虑到线程安全与阻塞,代码可进一步优化。

在多线程中,修改操作若为非原子有很大可能造成线程安全问题

因此,我们可以为put()和take()上锁打包成原子操作的。

其次,当队列未满或为空时,可以通过wait()和notify()进行阻塞和通知。

代码如下

class MyBlockingQueue{
    private String[] data=null;
    public MyBlockingQueue(int capacity){
        data=new String[capacity];
    }
    private int head=0;
    private int tail=0;
    private int size=0;

    public  void put(String s) throws InterruptedException {
        synchronized (this){
            if(size==data.length){
                //  return ;
                this.wait();
            }
            data[tail]=s;
            tail++;
            if(tail>=data.length){
                tail=0;;
            }
            size++;
            this.notify();
        }

    }

    public String take() throws InterruptedException {
        String ret="";
        synchronized (this){
            if(size==0){
               // return null;
                this.wait();
            }
            ret=data[head];
            head++;
            if(head>data.length){
                head=0;
            }
            size--;
        }
        this.notify();

        return ret;
    }

}

以上便是所有内容,如有不对,欢迎指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值