Java中BlockingQueue简介
1. 概述
在本文中,我们将研究解决并发生产者-消费者问题的最有用的构造之一java.util.concurrent。我们将研究*BlockingQueue * 接口的 API,以及来自该接口的方法如何使编写并发程序更容易。
在本文后面,我们将展示一个具有多个生产者线程和多个消费者线程的简单程序示例。
2. BlockingQueue类型
我们可以区分两种类型的BlockingQueue:
- 无界队列——几乎可以无限增长
- 有界队列——定义了最大容量
2.1. 无界队列
创建无界队列很简单:
BlockingQueue<String> blockingQueue = new LinkedBlockingDeque<>();
blockingQueue的容量将设置为*Integer.MAX_VALUE。*所有将元素添加到无界队列的操作都不会阻塞,因此它可能会增长到非常大的大小。
在使用无界 BlockingQueue 设计生产者-消费者程序时,最重要的是消费者应该能够在生产者将消息添加到队列时一样快地消费消息。否则,内存可能会填满,我们会收到OutOfMemory异常。
2.2. 有界队列
第二种队列是有界队列。我们可以通过将容量作为参数传递给构造函数来创建这样的队列:
BlockingQueue<String> blockingQueue = new LinkedBlockingDeque<>(10);
这里我们有一个容量等于 10 的阻塞队列。这意味着当生产者尝试将元素添加到已经满的队列时,取决于用于添加它的方法(offer()、add()或put() ),它将阻塞,直到插入对象的空间可用为止。否则,操作将失败。
使用有界队列是设计并发程序的好方法,因为当我们将元素插入到已经满的队列中时,操作需要等到消费者赶上并在队列中腾出一些可用空间。它使我们无需任何努力即可进行节流。
3. BlockingQueue API
BlockingQueue接口中有两种类型的方法——负责将元素添加到队列的方法和检索这些元素的方法。这两组中的每种方法在队列满/空的情况下表现不同。
3.1. 添加元素
- add() – 如果插入成功则返回true,否则抛出IllegalStateException
- put() - 将指定元素插入队列,如果需要,等待空闲槽
- offer() – 如果插入成功,则返回true,否则返回false
- offer(E e, long timeout, TimeUnit unit) – 尝试将元素插入队列并在指定的超时时间内等待可用的插槽
3.2. 检索元素
- take() – 等待队列的头元素并将其删除。如果队列为空,它会阻塞并等待元素变为可用
- poll(long timeout, TimeUnit unit) – 检索并移除队列的头部,如果有必要等待指定的等待时间以使元素可用。超时后返回null
在构建生产者-消费者程序时,这些方法是BlockingQueue接口中最重要的构建块。
4. 多线程生产者-消费者示例
让我们创建一个由两部分组成的程序——生产者和消费者。
Producer 将生成一个从 0 到 100 的随机数,并将该数字放入BlockingQueue中。我们将有 4 个生产者线程并使用*put()*方法阻塞,直到队列中有可用空间。
要记住的重要一点是,我们需要阻止我们的消费者线程无限期地等待元素出现在队列中。
从生产者向消费者发出没有更多消息要处理的信号的一种好方法是发送一个称为毒丸的特殊消息。我们需要发送尽可能多的毒丸,因为我们有消费者。然后,当消费者从队列中获取特殊的毒丸消息时,它将优雅地完成执行。
让我们看一个生产者类:
public class NumbersProducer implements Runnable {
private BlockingQueue<Integer> numbersQueue;
private final int poisonPill;
private final int poisonPillPerProducer;
public NumbersProducer(BlockingQueue<Integer> numbersQueue, int poisonPill, int poisonPillPerProducer) {
this.numbersQueue = numbersQueue;
this.poisonPill = poisonPill;
this.poisonPillPerProducer = poisonPillPerProducer;
}
public void run() {
try {
generateNumbers();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void generateNumbers() throws InterruptedException {
for (int i = 0; i < 100; i++) {
numbersQueue.put(ThreadLocalRandom.current().nextInt(100));
}
for (int j = 0; j < poisonPillPerProducer; j++) {
numbersQueue.put(poisonPill);
}
}
}
我们的生产者构造函数将BlockingQueue作为参数,用于协调生产者和消费者之间的处理。我们看到generateNumbers()方法会将 100 个元素放入队列中。它还需要毒丸消息,以了解在执行完成时必须将哪种类型的消息放入队列中。该消息需要被放入一个队列中的toxicPillPerProducer次。
每个消费者将使用take()方法从BlockingQueue中获取一个元素,因此它将阻塞,直到队列中有一个元素。从队列中取出一个整数后,它会检查消息是否是毒丸,如果是,则线程的执行完成。否则,它将在标准输出上打印出结果以及当前线程的名称。
这将使我们深入了解消费者的内部运作:
public class NumbersConsumer implements Runnable {
private BlockingQueue<Integer> queue;
private final int poisonPill;
public NumbersConsumer(BlockingQueue<Integer> queue, int poisonPill) {
this.queue = queue;
this.poisonPill = poisonPill;
}
public void run() {
try {
while (true) {
Integer number = queue.take();
if (number.equals(poisonPill)) {
return;
}
System.out.println(Thread.currentThread().getName() + " result: " + number);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
需要注意的重要一点是队列的使用。与生产者构造函数相同,队列作为参数传递。我们可以这样做,因为BlockingQueue可以在线程之间共享而无需任何显式同步。
现在我们有了生产者和消费者,我们可以开始我们的程序了。我们需要定义队列的容量,并将其设置为 100 个元素。
我们希望有 4 个生产者线程,消费者线程的数量将等于可用处理器的数量:
int BOUND = 10;
int N_PRODUCERS = 4;
int N_CONSUMERS = Runtime.getRuntime().availableProcessors();
int poisonPill = Integer.MAX_VALUE;
int poisonPillPerProducer = N_CONSUMERS / N_PRODUCERS;
int mod = N_CONSUMERS % N_PRODUCERS;
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(BOUND);
for (int i = 1; i < N_PRODUCERS; i++) {
new Thread(new NumbersProducer(queue, poisonPill, poisonPillPerProducer)).start();
}
for (int j = 0; j < N_CONSUMERS; j++) {
new Thread(new NumbersConsumer(queue, poisonPill)).start();
}
new Thread(new NumbersProducer(queue, poisonPill, poisonPillPerProducer + mod)).start();
BlockingQueue是使用具有容量的构造创建的。我们正在创建 4 个生产者和 N 个消费者。我们将毒丸消息指定为Integer.MAX_VALUE,因为在正常工作条件下,我们的生产者永远不会发送这样的值。这里最需要注意的是BlockingQueue用于协调它们之间的工作。
当我们运行程序时,4 个生产者线程将随机整数放入BlockingQueue中,消费者将从队列中取出这些元素。每个线程都会将线程的名称连同结果一起打印到标准输出。