阻塞队列 (BlockingQueue)

阻塞队列

什么是阻塞队列

说到队列, 第一时间想到的队列的特性就是: 一种 “先进先出” 的数据结构
而阻塞队列是一种特殊的队列, 也继承了队列的特性: 先进先出.

  • 但是在其先进先出的特性的基础上, 阻塞队列拥有另一种特殊的功能, 顾名思义, 就是: 阻塞
    它在普通队列的基础上额外拥有以下几个特性:
  1. 如果队列为空, 执行出队列的操作, 就会阻塞, 阻塞到另一个线程往里面添加元素为止 (队列不空)
  2. 如果队列满了, 执行入队列的操作, 也会阻塞, 阻塞到另一个线程从里面取走元素为止 (队列不满)
BlockingQueue 的使用

Java 标准库中提供了一个接口 (BlockingQueue), 其中包含了几种阻塞队列的实现:

可以看到, 标准库中的阻塞队列有好几种实现方式, 有基于数组实现的 ArrayBlockingQueue, 有基于链表实现的 LinkedBlockingQueue, 有带有优先级的阻塞队列 PriorityBlockingQueue 等等…

阻塞队列也给我们提供了具体的操作阻塞队列的方法:

可以看到, 标准库提供的阻塞队列中带有阻塞作用的方法是 put() 和 take()

  • 注意: offer() 和 poll() 是不带有阻塞的方法
    下面简单的使用一下标准库的阻塞队列:
public class ThreadDemo21 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String > blockingQueue = new LinkedBlockingQueue<>();
        blockingQueue.put("1");
        blockingQueue.put("2");
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
    }
}

上述代码中, 使用 put() 方法往队列中添加了 “1” 和 “2” 两个元素, 并使用 take() 方法取出元素三次并打印

可以看到控制台中打印了两个元素, 并且产生了阻塞的效果, 如果当前有另一个线程往队列中添加元素的时候, 这个程序就能立刻取出第三个元素并打印

使用 BlockingQueue 来完成生产者消费者模型

生产者-消费者模型是一个经典的多线程模型, 由两类线程和一个缓冲区组成

  • 生产者作为一类线程, 负责生产数据, 并将数据放入缓冲区中
  • 缓冲区作为一组中间状态的组件, 可以使用阻塞队列来作为缓冲区, 或者使用一些中间件, 如: 消息队列 (Kafka, RabbitMQ…)
  • 消费者作为另一类线程, 负责消费数据. 消费者会从缓冲区接收数据并将接收的数据进行消费

这里使用 BlockingQueue 来作为缓冲区完成基本的生产者-消费者模型:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ThreadDemo22 {
    public static void main(String[] args) {
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();

        // 创建两个线程, 来作为生产者和消费者
        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    Integer res = blockingQueue.take();
                    System.out.println("消费元素: " + res);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        customer.start();

        Thread producer = new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    blockingQueue.put(count);
                    System.out.println("生产元素: " + count);
                    count++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();
    }
}

上述代码, 创建了两个线程 t1 和 t2 分别作为生产者和消费者线程, 使用 blockingQueue 来作为数据缓冲区, t2 负责生产数据, 每隔 500ms 往队列中添加一个元素, 而 t1 负责消费数据, 会不间断的向队列索要元素, 当 t2 往队列中添加元素的时候, t1 就会直接把这个元素取走 (当然, 消费者消费的速度是可以通过程序来控制的)

当运行上述程序之后, 可以看到, 生产者源源不断的生产出数据, 而消费者也会取走数据. 生产者, 消费者, 缓冲区 这三个元素共同构建出了生产者-消费者模型

自定义阻塞队列

想要实现一个阻塞队列, 首先需要先写一个普通的队列.
队列可以基于数组或者链表来实现, 使用链表更容易进行头插和尾删的操作.
这里使用数组来实现一个循环队列:

  1. 定义一个 item[] 数组, 数组长度默认为 100
  2. 使用 head 和 tail 两个节点分别指向头结点和尾结点
  3. 使用 size 来存储数组中存放了多少个元素
  4. 当 head 跟 tail 指向的结点为同一个结点的时候, 如果此时 size == 0 队列为空, 此时 size == item.length 的话队列为满.

基于上述属性来写一个循环队列:

class MyBlockingQueue {
    private int[] items = new int[100];
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    // 入队列
    public void put(int val) throws InterruptedException {
        if (size == items.length) {
            // 队列满了
            return;
        }
        items[tail] = val;
        tail++;
 
        if (tail >= items.length) {
            tail = 0;
        }
        size++; 
    }

    // 出队列
    public Integer take() throws InterruptedException {
        int res = 0;
        if (size == 0) {
            // 队列空
            return null;
        }
        res = items[head];
        head++;
        if (head >= items.length) {
            head = 0;
        }
        size--;
        
        return res;
    }
}

基于上面所写的循环队列, 可以将它升级为阻塞队列, 需要给它加入阻塞功能
加入阻塞功能, 就代表着需要在多线程环境下来使用这个队列, 由于里面的入队列和出队列方法都涉及到读写操作, 所以就需要给这两个方法加上锁

接下来给这个队列加入阻塞功能:

  • 使用 wait 和 notify 来互相配合
  • 当检测到队列为空的时候要执行出队列操作, 就使用 wait 使出队列操作阻塞, 当有另外的线程往里面添加元素的时候, 使用 notify 通知刚才的 wait, 让它在队列非空的情况下执行出队列操作
  • 当检测到队列为满的时候要执行入队列操作, 就是用 wait 使入队列操作阻塞, 当有另外的线程往里面取走元素的时候, 使用 notify 通知刚才的 wait, 让它在队列不满的情况下执行入队列操作

这里有一个小问题, 有没有可能, 当入队列之后通知出队列的线程执行出队列的操作的时候, 这个时候会不会出现队列还是空的情况呢?
当然在这个程序中是不可能出现这种情况的, 但是作为程序员, 为了稳妥起见, 最好的办法是在 wait 返回之后再次判断条件是否具备

可以看一下标准库中 wait 的注释, 可以看到标准库中推荐的写法:

  • 使用 while 循环来判定是否满足条件, 不满足则继续进行 wait 操作

这样, 我们自定义的阻塞队列就大功告成了:

class MyBlockingQueue {
    private int[] items = new int[100];
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    // 入队列
    public synchronized void put(int val) throws InterruptedException {
        while (size == items.length) {
            // 队列满了
            // return;
            // 加入阻塞操作
            this.wait();
        }
        items[tail] = val;
        tail++;
        // 针对 tail 的处理
        if (tail >= items.length) {
            tail = 0;
        }
        size++;
        
        // 唤醒 take 中的 wait
        this.notify();
    }

    // 出队列
    public synchronized Integer take() throws InterruptedException {
        while (size == 0) {
            // 队列空
            // return null;
            // 加入阻塞操作
            this.wait();
        }
        int res = items[head];
        head++;
        if (head >= items.length) {
            head = 0;
        }
        size--;

        // 唤醒 put 中的wait
        this.notify();
        return res;
    }
}
使用自定义阻塞队列来完成生产者消费者模型
public class ThreadDemo23 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue queue = new MyBlockingQueue();

        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    int res = queue.take();
                    System.out.println("消费元素: " + res);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        customer.start();

        Thread producer = new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    System.out.println("生产元素: " + count);
                    queue.put(count);
                    count++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();
    }
}

得到结果:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值