1.介绍
本文介绍,解决并发生产者-消费者问题的最有用的构造 java.util.concurrent 之一。 将查看 BlockingQueue 接口的 API 以及该接口中的方法如何使编写并发程序更容易。
2.阻塞队列类型
可以区分两种类型的 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.阻塞队列API
BlockingQueue 接口中有两种类型的方法——负责将元素添加到队列的方法和检索这些元素的方法。 在队列已满/空的情况下,这两组中的每种方法的行为都不同。
3.1. 添加元素
- add() – 如果插入成功则返回 true,否则抛出 IllegalStateException
- put() – 将指定元素插入队列,必要时等待空闲槽
- offer() – 如果插入成功则返回真,否则返回假
- 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;
}
@Override
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);
}
}
}
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;
}
@Override
public void run() {
try {
while (true) {
Integer number = queue.take();
if (number.equals(poisonPill)) {
System.out.println("posionPill");
return;
}
System.out.println(Thread.currentThread().getName() + " result: " + number);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
测试
@Test
public void test1() throws InterruptedException {
int BOUND = 10;
int N_PRODUCERS = 4;
int N_CONSUMERS = Runtime.getRuntime().availableProcessors();
System.out.println("N_CONSUMERS:"+N_CONSUMERS);
int poisonPill = Integer.MAX_VALUE;
int poisonPillPerProducer = 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();
}
Thread.sleep(5000);
}