Java 多线程案例-阻塞队列

目录

一、阻塞队列是什么

二、什么是生产者消费者模型

2.1概念

2.2作用

三、Java标准库提供的阻塞队列的使用

四、模拟实现阻塞队列

一、阻塞队列是什么

阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则.

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.

当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素. 阻塞队列的一个典型应用场景就是 "生产者消费者模型".

这是一种非常典型的开发模型.

二、什么是生产者消费者模型

2.1概念

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

2.2作用

        先来看一个例子:假设现在过年一家人在包饺子,这时候一部分人擀饺子皮,一部分人包饺子,擀饺子皮的人会把擀好的饺子皮放到案板上,包饺子的人直接从案板上取饺子皮就可以。在这个例子中,生产饺子皮的人就是生产者,包饺子的人消费饺子皮,就是消费者,而案板就是扮演了阻塞队列这个角色。

作用:

        1.生产者消费者模式本质就是借助一个容器来解决的解决生产者和消费者的强耦合问题,我们在编写代码的时候,往往是追求高内聚,低耦合,降低代码之间的关联性,这样才不会导致一部分代码出问题时,整个程序就挂了。在这里我们借助阻塞队列,让阻塞队列作为一个中间者,这时候如果生产者或者消费者出现问题时,就不会导致程序直接挂了。

        2.生产者消费者模型还有利于削峰填谷,这个我们可以这样理解:我们有个A服务器,用来接收用户的请求,B服务器用来处理用户的请求。这时候假设没有阻塞队列来作为中间者,如果这时候A服务器突然收到很多用户请求,B服务器处理的也会突然增加,这时候B服务器就可能会直接卡死,B就挂了。这时候假设有个阻塞队列用于接收A的请求,将A的请求放到队列当中,就算A请求突然大增,就可以先放到队列当中,当队列满时,A就会先阻塞的等待,B还是可以按照原来的速度一个一个去处理,不会出现直接崩溃的情况。(阻塞队列是功能比较简单的,所以队列挂的概率比较低)。

大概模型: 

Java 多线程案例-阻塞队列_第1张图片

三、Java标准库提供的阻塞队列的使用

        Java中提供的阻塞队列就是:BlockingQueue,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue和LinkedBlockingQueue等等。

BlockingQueue的用法:

代码案例:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadDemo18 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue queue = new LinkedBlockingQueue<>();
        Thread t1 = new Thread(()->{
            int val = 0;
            while(true){//每秒生产一个元素
                try {
                    queue.put(val);
                    System.out.println("生产元素:"+val);
                    val++;
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(()->{
            while(true){//作为消费者,只要有元素就消费,没有元素就阻塞等待
                try {
                    int val = queue.take();
                    System.out.println("消费元素:"+val);
                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
            }
        });
        t2.start();
        t1.join();
        t2.join();
    }
}

运行结果:

Java 多线程案例-阻塞队列_第2张图片

        分析:上面的代码就是BlockingQueue的简单使用方法,我们使用循环的方法一秒生成元素作为生产者,消费者直接循环消费不做任何限制,只要有元素就消费,这也是简单的生产者消费者模型。BlockingQueue的使用方法比较简单,我们接下来模拟实现一下BlockingQueue。

四、模拟实现阻塞队列

        思路:阻塞队列与普通队列的区别就在于可以实现阻塞功能,阻塞队列还具备线程安全。因此,我们就可以这样做:先实现一个普通队列,在普通队列的基础上保证线程安全,最后再加上阻塞功能就可以了。这里我们实现一个整数类型的阻塞队列:

代码实现:

class MyBlockingQueue{
    volatile private int[] elem = new int[1000];//存储1000个元素的数组
    volatile private int front = 0;//队头
    volatile private int rear = 0;//队尾
    volatile private int size = 0;//元素个数
    synchronized public void put(int val) throws InterruptedException {//添加元素
        while(size == elem.length){//因为wait可能会被打断,所以需要循环判断
            this.wait();
        }
        if(rear == elem.length){//到队尾了就从头开始
            rear = 0;
        }
        elem[rear++] = val;
        size++;//有元素入队列了,可以唤醒正要出队列的方法了
        this.notify();
    }

    synchronized public int take() throws InterruptedException {//出队列
        while(size == 0){//为了防止wait被突然唤醒,可以循环判断队列是否为空
            this.wait();
        }
        if(front == elem.length){//到队尾了就重新回到队头,形成循环队列
            front = 0;
        }
        size--;
        int val = elem[front++];
        this.notify();//有元素出队列了,就可以唤醒正在阻塞的put方法了
        return val;//返回队头元素
    }

    public int getSize(){//获取队列长度
        return size;
    }

}

        分析:首先是循环队列我们采用记录元素个数的方法来实现,只要到达队尾了就让rear或者front回到0下标位置。对于有元素要入队列,这里会涉及到对size这个值和rear进行修改,我们直接对整个方法进行加锁,对于出队列操作,也会对size和front进行修改,所以我们也对整个方法进行加锁操作,对数组和记录队头队尾和数组长度的变量都保证其内存可见性。这样子我们这个线程安全了。对于阻塞效果我们采用wait()和notify()来实现,当队列满的时候就等待,当有元素出队列的时候就唤醒;当队列空时也等待,当有元素入队列时就唤醒,这样子就实现了队列的阻塞效果了,出队列和入队列的操作就是相互唤醒的。

        注意事项:这里的循环等待是不能修改,因为在多线程代码中,我们不能保证wait会不会被提起那唤醒,如果被提前唤醒了,就可能会出现错误,因此需要使用循环判断来阻塞等待。

你可能感兴趣的:(java,jvm,开发语言)