在Java并发编程中,生产者-消费者问题是一个常见的需求。java.util.concurrent.BlockingQueue接口为解决这类问题提供了强大便捷的支持。
不仅提供了在多线程环境下安全添加和获取元素的方法,还通过阻塞机制确保了生产者和消费者之间的协调。
接下来,我们一起看看BlockingQueue的特性、方法以及如何使用它构建高效的多线程应用程序。
一、BlockingQueue类型
(一)无界队列
无界队列可以在理论上无限增长,在Java中创建无界BlockingQueue非常简便:
BlockingQueue<String> unboundedQueue = new LinkedBlockingDeque<>();
此队列的容量默认是Integer.MAX_VALUE,虽然有默认边界,但在实际应用中,若生产者持续快速生产元素,而消费者无法及时消费,可能导致内存占用不断增加,最终引发OOM。所以说,虽然默认有界,实际相当于无界。
(二)有界队列
有界队列则具有明确的最大容量限制,创建方式如下:
BlockingQueue<Integer> boundedQueue = new LinkedBlockingDeque<>(10);
这里创建了一个容量为10的BlockingQueue。
当生产者尝试向已满的有界队列添加元素时,添加方法(比如put()),操作可能会阻塞,直到队列中有可用空间。这种特性使得有界队列在某些场景下,能自动实现限流,避免系统资源过度消耗。
二、BlockingQueue的方法
(一)添加元素
add(E e)
尝试将指定元素添加到队列中。若添加成功,返回true;若队列已满,则抛出IllegalStateException异常。比如:
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
try {
boolean success = queue.add(10);
if (success) {
System.out.println("元素添加成功");
}
} catch (IllegalStateException e) {
System.out.println("队列已满,添加元素失败");
}
put(E e)
将元素插入队列,如果队列已满,则阻塞当前线程,直到有可用空间。比如:
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
try {
queue.put(20);
System.out.println("元素已放入队列");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程被中断,添加元素失败");
}
offer(E e)
尝试将元素添加到队列中。若添加成功,返回true;若队列已满,则返回false。比如:
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
boolean result = queue.offer(30);
if (result) {
System.out.println("元素添加成功");
} else {
System.out.println("队列已满,添加元素失败");
}
offer(E e, long timeout, TimeUnit unit)
在指定的超时时间内尝试将元素插入队列。若在超时时间内成功插入,返回true;否则返回false。比如:
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
try {
boolean success = queue.offer(40, 2, TimeUnit.SECONDS);
if (success) {
System.out.println("元素在超时时间内添加成功");
} else {
System.out.println("在超时时间内未能添加元素,队列可能已满");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程被中断,添加元素失败");
}
(二)检索元素
take()
从队列中获取并移除头部元素。如果队列为空,当前线程将被阻塞,直到有元素可用。
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
// 假设队列中已有元素
try {
Integer element = queue.take();
System.out.println("取出的元素为: " + element);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程被中断,获取元素失败");
}
poll(long timeout, TimeUnit unit)
检索并移除队列头部元素。若在指定的超时时间内有元素可用,则返回该元素;若超时时间内仍无元素可用,则返回null。
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
try {
Integer element = queue.poll(1, TimeUnit.SECONDS);
if (element!= null) {
System.out.println("在超时时间内取出的元素为: " + element);
} else {
System.out.println("在超时时间内未获取到元素,队列可能为空");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("线程被中断,获取元素失败");
}
三、多线程生产者-消费者
我们将模拟一个简单的生产-消费场景,假设有一个消息队列,多个生产者线程不断向队列中生产消息(这里简化为随机整数),多个消费者线程从队列中获取消息并进行处理(这里简化为打印消息和线程名)。为了能够有效结束,增加poisonPill参数。
首先是生产者:
public class Producer implements Runnable {
private final BlockingQueue<Integer> queue;
private final int poisonPill;
private final int poisonPillPerProducer;
public Producer(BlockingQueue<Integer> queue, int poisonPill, int poisonPillPerProducer) {
this.queue = queue;
this.poisonPill = poisonPill;
this.poisonPillPerProducer = poisonPillPerProducer;
}
@Override
public void run() {
try {
produceMessages();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void produceMessages() throws InterruptedException {
Random random = new Random();
for (int i = 0; i < 100; i++) {
int message = random.nextInt(100);
queue.put(message);
}
for (int j = 0; j < poisonPillPerProducer; j++) {
queue.put(poisonPill);
}
}
}
生产者构造函数接受一个BlockingQueue用于与消费者通信,还接受poisonPill和每个生产者应发送poisonPill值的数量。在produceMessages方法中,先生产100个随机消息放入队列,然后发送指定数量的毒丸。
接着是消费者:
public class Consumer implements Runnable {
private final BlockingQueue<Integer> queue;
private final int poisonPill;
public Consumer(BlockingQueue<Integer> queue, int poisonPill) {
this.queue = queue;
this.poisonPill = poisonPill;
}
@Override
public void run() {
try {
consumeMessages();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void consumeMessages() throws InterruptedException {
while (true) {
Integer message = queue.take();
if (message.equals(poisonPill)) {
return;
}
System.out.println(Thread.currentThread().getName() + " 消费消息: " + message);
}
}
}
消费者构造函数接受BlockingQueue和poisonPill值。在consumeMessages方法中,不断从队列获取消息,如果是等于poisonPill则结束消费,否则打印消息和线程名。
最后是主程序类来启动生产者和消费者线程:
final int BOUND = 10;
final int N_PRODUCERS = 3;
final int N_CONSUMERS = 2;
final int poisonPill = Integer.MAX_VALUE;
final int poisonPillPerProducer = N_CONSUMERS / N_PRODUCERS;
final int mod = N_CONSUMERS % N_PRODUCERS;
final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(BOUND);
for (int i = 0; i < N_PRODUCERS; i++) {
new Thread(new Producer(queue, poisonPill, poisonPillPerProducer)).start();
}
for (int j = 0; j < N_CONSUMERS; j++) {
new Thread(new Consumer(queue, poisonPill)).start();
}
new Thread(new Producer(queue, poisonPill, poisonPillPerProducer + mod)).start();
在主程序中,定义了队列容量、生产者和消费者数量,以及poisonPill值相关参数,创建了数量为10的有界BlockingQueue,启动了指定数量的生产者和消费者线程。
当运行上述程序时,生产者线程会不断向队列中放入随机整数,消费者线程会从队列中取出并打印这些整数,同时每个消费者接收到poisonPill后会结束执行。
运行结果如下(因为用了随机数,每次效果不同):
Thread-3 消费消息: 47
Thread-4 消费消息: 3
Thread-3 消费消息: 35
Thread-4 消费消息: 83
Thread-4 消费消息: 68
Thread-4 消费消息: 40
Thread-4 消费消息: 73
Thread-4 消费消息: 56
Thread-4 消费消息: 56
...
随着生产和消费的进行,最终所有消费者线程在接收到poisonPill后停止。