设计模式之一——堵塞队列

堵塞队列呢是一种更为复杂的队列,他对比简单的队列有两个特性:1)线程安全;2)堵塞:a)队列为空时,尝试出队列,出队列操作就会堵塞,直到有新的元素添加进来为止;b)队列满了时,尝试入队列操作时会发生堵塞,直至队列被取走为止。

生产者消费者模型

堵塞队列呢有一个非常主要的场景:实现"生产者消费者模型"。什么是生产者消费者模型呢?

就拿这个服务器举例:这里直接A服务器请求B服务器,B服务器响应A服务器这种就不是生产者消费者模型。

当我们加入一个中转栈的时候就是实现了生产者消费者模型了,当A服务器想要去请求B服务器时,A服务器直接在中间栈中取即可,B服务器的响应也是直接发送给中间栈即可,这个中间栈呢也是我们今天要学的------堵塞队列

生产者消费者模型的优缺点

1.解耦合

通过上述服务器与服务器之间呢我们可以发现,当我们需要更改服务器A/B时,服务器B/A也会受到影响,这时这两个服务器就是高耦合了,但如果加入堵塞队列,我们更改某个服务器的代码时就不需要管另一个服务器的业务了,他们之间通过了堵塞队列来连接,而堵塞队列里的逻辑也不会有这两个服务器业务复杂。

2.削峰填谷

我们上过大学都知道,每当抢课时学校的官网都会崩溃,这就是因为进入官网的人太多了,所以A服务器一般都会有某个时刻点进去的人多,但B服务器的响应又有限,所以当我们直接连接两个服务器时可能造成服务器崩溃。加入了堵塞队列后,当点击A服务器的人多了就会对堵塞队列请求的多,但B服务器还是以之前的速度响应,不会影响到B服务器,但趁着这个峰值过去了服务器B还是以之前的响应速度响应传给堵塞队列,故而等下次点击量波峰时可以有效的缓解。

3.缺点

1.使代码变得复杂

引入队列之后,整体的结果会变得更复杂,此时,就需要更多的机器来部署,生产环境的结果复杂,管理起来也复杂。

2.效率降低

不使用堵塞队列时,服务器与服务器之间的请求和响应都是直接的,引入了堵塞队列他们还得加载到队列中,当数量达到一定量时会影响效率。

堵塞队列的使用

BlockingQueue的介绍

堵塞队列的类是BlockingQueue:

通过观察我们发现,new时会给我们很多对象,这里我们就讨论三个:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue;

ArrayBlockingQueue:这时一个基于数组实现的堵塞队列;

LinkedBlockingQueue:这是一个基于链表实现的堵塞队列;

PriorityBlockingQueue:这时基于优先级队列实现的堵塞队列;

BlockingQueue这个类呢我们点击去发现他是继承了Queue类的,使用当我们使用这个类时是可以当作普通类来使用的;但我们学BlockingQueue的目的是学习堵塞队列,故而我们就讲讲堵塞队列的使用。

BlockingQueue的使用

这样子我们就简单的创建了一个容量为100的堵塞队列。

当我们使用put方法时就是往堵塞队列中加入元素了,当队列满了在添加时就会堵塞等待。

弹出队列是take(),当队列为空时就会堵塞等待。

堵塞队列的使用就讲到这里,我们重点讲解堵塞队列的实现!!!!

堵塞队列的实现

堵塞队列的实现主要涉及到一下几个问题:

1)怎么插入元素

2)怎么取出元素

3)怎么实现堵塞

1.插入与删除元素

我们实现堵塞队列主要以数组的方式来实现,插入和删除元素可以定义一个头指针和尾指针,插入与删除就是头指针和尾指针加减。

2.怎么实现堵塞

我们之前学过wait和notify,这两个就可以很好的实现堵塞等待,当数组满了的时候就使用wait等待,等到有元素的删除即可发出notify来截至堵塞。空队列也是如此。

代码的实现

复制代码
class MyBlockingQueue{
    private String[] date = null;
    private int head = 0;//头指针
    private int tail = 0;//尾指针
    private int size = 0;//元素个数

    public MyBlockingQueue(int capacity) {
        date = new String[capacity];
    }
    public void put(String elem){
        if(size >= date.length){
            //堵塞等待
        }
        date[tail] = elem;
        tail++;
        if(tail >= date.length){
            tail = 0;
            //因为队列是先进先出的,当tail大于或等于了数组长度时,
            // 说明数组添加元素从头开始了,这里也不要害怕head没有走,因为有堵塞等待。
        }
    }
    public String take(){
        if(size == 0){
            //队列为堵塞等待
        }
        String ret = date[head];
        head++;
        if(head >= date.length){
            head =0;
            //因为队列是先进先出的,当队列走到了尽头的时候数组,
            // 因为put是从头开始来的,所以令head为0,
            // 当然也不要担心head和tail撞见,因为有堵塞等待
        }
        size--;
        return ret;
    }
}

第一步,我们设计出了基本的框架,这时插入和删除已经完成,但还差堵塞等待:我们得考虑wtai和notify在哪里加入?

复制代码
class MyBlockingQueue{
    private String[] date = null;
    private int head = 0;//头指针
    private int tail = 0;//尾指针
    private int size = 0;//元素个数

    public MyBlockingQueue(int capacity) {
        date = new String[capacity];
    }
    public void put(String elem) throws InterruptedException {
        synchronized (this){
            if(size >= date.length){
                this.wait();
                //堵塞等待
            }
            date[tail] = elem;
            tail++;
            if(tail >= date.length){
                tail = 0;
                //因为队列是先进先出的,当tail大于或等于了数组长度时,
                // 说明数组添加元素从头开始了,这里也不要害怕head没有走,因为有堵塞等待。
            }
            size++;
            this.notify();
        }

    }
    public String take() throws InterruptedException {
        synchronized (this){
            if(size == 0){
                this.wait();
                //队列为堵塞等待
            }
            String ret = date[head];
            head++;
            if(head >= date.length){
                head =0;
                //因为队列是先进先出的,当队列走到了尽头的时候数组,
                // 因为put是从头开始来的,所以令head为0,
                // 当然也不要担心head和tail撞见,因为有堵塞等待
            }
            size--;
            this.notify();
            return ret;
        }
    }
}

wait和notify呢可以加的也很简单,wait可以直接在 if(size == 0)和 if(size >= date.length)里边加,因为这里就是为了他们堵塞等待的,notify呢可以加载最后面,因为此时也快要解锁了让这两个方法能更好的竞争锁。

但这里还面临这一个问题,我们知道堵塞是可以通过其他方法唤醒的,比如Interrupt,当wait被其他方法唤醒时会出现队列满了/空了也会执行下去造成越界,而出现bug,所以我们可以把两个if改成while循环,此时即使wait提前被唤醒了也还会经过while判断是否成立,成立即退出,不成立则继续循环,故而最终代码如下:

复制代码
class MyBlockingQueue {
    private String[] data = null;

    // 队首
    private int head = 0;

    // 队尾
    private int tail = 0;

    // 元素个数
    private int size = 0;


    public MyBlockingQueue(int capacity) {
        data = new String[capacity];
    }

    public void put(String elem) throws InterruptedException {
        synchronized (this) {
            while (size >= data.length) {
                // 队列满了. 需要阻塞的
                // return;
                this.wait();
            }
            data[tail] = elem;
            tail++;
            if (tail >= data.length) {
                tail = 0;
            }

            // tail = (tail + 1) % data.length;

            size++;
            this.notify();
        }
    }

    public String take() throws InterruptedException {
        synchronized (this) {
            while (size == 0) {
                // 队列空了. 需要阻塞
                // return null;
                this.wait();
            }
            String ret = data[head];
            head++;
            if (head >= data.length) {
                head = 0;
            }
            size--;
            this.notify();
            return ret;
        }
    }
}
相关推荐
触底反弹2 小时前
🤯 面试被问 AI Workflow 和 Agent 有啥区别?3 张图 + 2 段代码讲清楚!
人工智能·设计模式·面试
杨充1 天前
10.可测试性实战设计
设计模式·开源·代码规范
杨充1 天前
9.重构十二式的实战
设计模式·开源·代码规范
杨充1 天前
6.设计原则的全景图
设计模式·开源·全栈
杨充1 天前
2.面向对象的特性
设计模式
杨充1 天前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充1 天前
8.反模式与坏味道
设计模式·开源·代码规范
杨充1 天前
3.接口vs抽象类比较
设计模式
咖啡八杯1 天前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法