前面用锁和条件队列实现过一个只能放一个值的盒子。需要缓冲多个任务时,可以直接用 BlockingQueue,不用自己维护等待和唤醒。
用有界队列交付任务
下面只有一个生产者、一个消费者。数字表示普通任务,-1 是约定好的结束标记:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumerDemo {
private static final int END = -1;
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(2);
Thread consumer = new Thread(() -> {
try {
while (true) {
int value = queue.take();
if (value == END) break;
System.out.println("处理 " + value);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "consumer");
consumer.start();
try {
for (int i = 1; i <= 5; i++) queue.put(i);
queue.put(END);
consumer.join();
} finally {
consumer.interrupt();
}
}
}正常运行会依次处理 1 到 5,然后结束。队列满时主线程在 put() 等待,队列空时消费者在 take() 等待。
这里用 FIFO 队列,结束标记排在普通任务后面,消费者可以把前面的任务处理完。finally 中的中断用于主线程异常退出时通知消费者结束,避免它一直等在空队列上;主线程中途退出时,队列里可能还有未处理的任务。
队列满了怎么办
put() 一直等空位,适合允许生产者被反向限制的情况。面向请求的任务提交,可能更适合限时等待:offer(task, 100, TimeUnit.MILLISECONDS)。
返回 false 时要明确处理结果。不能悄悄忽略,随后又告诉调用方“任务已经提交成功”。队列容量也不是越大越好,积压越多,排队时间和内存占用通常越高。
结束标记不是通用关闭方法
BlockingQueue 没有统一的 close()。上面的结束标记,是应用层自己约定的协议。
如果有多个消费者,需要确保每个消费者都能收到结束通知,例如每个消费者一个标记。还必须先确认所有生产者都结束,再发送这些标记,否则后面追加的任务可能无人处理。
如果消费者提前失败,而生产者一直 put(),队列满后生产者就可能永久等待。这种情况下需要把失败通知给生产者,或者让提交操作超时返回。
一个任务是否只处理一次
队列把元素交给一个消费者,不代表业务一定“恰好执行一次”。消费者取出任务后崩溃,任务可能丢失;处理成功后没有来得及确认,外部重试又可能重复执行。
内存队列里的任务会随着进程退出而丢失。如果要求重启后继续处理,需要另外保存任务;允许重试时,也要处理重复执行的问题。