前面用锁和条件队列实现过一个只能放一个值的盒子。需要缓冲多个任务时,可以直接用 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(),队列满后生产者就可能永久等待。这种情况下需要把失败通知给生产者,或者让提交操作超时返回。

一个任务是否只处理一次

队列把元素交给一个消费者,不代表业务一定“恰好执行一次”。消费者取出任务后崩溃,任务可能丢失;处理成功后没有来得及确认,外部重试又可能重复执行。

内存队列里的任务会随着进程退出而丢失。如果要求重启后继续处理,需要另外保存任务;允许重试时,也要处理重复执行的问题。

最后修改:2018 年 03 月 18 日
如果觉得我的文章对你有用,请随意赞赏