BlockingQueue使用指南-Java快速入门教程
1. 概述
在本文中,我们将研究最有用的构造之一java.util.concurrent来解决并发生产者-消费者问题。我们将研究BlockingQueue接口的 API 以及该接口中的方法如何使编写并发程序更容易。
在本文的后面,我们将展示一个具有多个生产者线程和多个使用者线程的简单程序的示例。
2.阻塞队列类型
我们可以区分两种类型的阻塞队列:
- 无限队列 – 几乎可以无限增长
- 有界队列 – 定义了最大容量
2.1. 无限队列
创建无限队列很简单:
BlockingQueue<String> blockingQueue = new LinkedBlockingDeque<>();
blockingQueue的容量将设置为Integer.MAX_VALUE。将元素添加到无限队列的所有操作都不会阻塞,因此它可能会增长到非常大的大小。
在使用无界 BlockingQueue 设计生产者-消费者程序时,最重要的事情是,消费者应该能够在生产者将消息添加到队列时尽快使用消息。否则,内存可能会填满,我们将出现内存不足异常。
2.2. 有界队列
第二种类型的队列是有界队列。我们可以通过将容量作为参数传递给构造函数来创建这样的队列:
BlockingQueue<String> blockingQueue = new LinkedBlockingDeque<>(10);
这里我们有一个容量等于 10 的阻塞队列。这意味着当生产者尝试将元素添加到已经满的队列中时,根据用于添加它的方法(offer(),add()或put()),它将阻塞,直到插入对象的空间可用。否则,操作将失败。
使用有界队列是设计并发程序的好方法,因为当我们将元素插入到已经满的队列中时,该操作需要等到消费者赶上并在队列中腾出一些可用空间。它为我们提供了节流,而无需我们付出任何努力。
3.阻塞队列接口
BlockingQueue接口中有两种类型的方法 - 负责将元素添加到队列的方法和检索这些元素的方法。如果队列已满/为空,这两个组中的每种方法的行为都不同。
3.1. 添加元素
- add() – 如果插入成功,则返回true,否则抛出IllegalStateException
- put() – 将指定的元素插入队列,必要时等待空闲插槽
- offer() – 如果插入成功,则返回true,否则返回false。
- offer(E e, long timeout, TimeUnit unit) – 尝试将元素插入队列并在指定超时内等待可用插槽
3.2. 检索元素
- take() – 等待队列的 head 元素并将其删除。如果队列为空,它将阻塞并等待元素变为可用
- poll(long timeout, TimeUnit unit) – 检索并删除队列的头部,如有必要,等待指定的等待时间以使元素可用。超时后返回空值
这些方法是构建生产者-消费者程序时BlockingQueue接口中最重要的构建块。
4. 多线程生产者-消费者示例
让我们创建一个由两部分组成的程序 - 生产者和消费者。
生产者将产生一个从 0 到 100 的随机数,并将该数字放入阻塞队列中。我们将有 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个元素放入队列中。它还需要毒丸消息,以了解在执行完成时必须将哪种类型的消息放入队列中。该消息需要将poisonPillPerProducer时间放入队列中。
每个消费者将使用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,消费者将从队列中获取这些元素。每个线程将打印到标准输出线程的名称以及结果。
5. 结论
本文展示了BlockingQueue的实际用法,并解释了用于从中添加和检索元素的方法。此外,我们还展示了如何使用BlockingQueue构建多线程生产者-消费者程序来协调生产者和消费者之间的工作。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix