具有停止条件的Java生产者 - 消费者

时间:2013-05-16 16:20:52

标签: java concurrency producer-consumer

我有N个工作人员共享要计算的元素队列。在每次迭代中,每个worker都会从队列中删除一个元素,并且可以生成更多要计算的元素,这些元素将被放在同一个队列中。基本上,每个生产者也是消费者。当队列中没有元素并且所有工作者已经完成计算当前元素时,计算结束(因此不能再生成要计算的元素)。我想避免调度员/协调员,所以工人应该协调。允许工人查明暂停条件是否有效的最佳模式是什么,因此代表其他人暂停计算?

例如,如果所有线程都只执行此循环,那么当所有元素都被计算出来时,它将导致所有线程被永久阻塞:

while (true) {
    element = queue.poll();
    newElements[] = compute(element);
    if (newElements.length > 0) {
        queue.addAll(newElements);
    }
}

1 个答案:

答案 0 :(得分:6)

保持活动线程的数量。

public class ThreadCounter {
    public static final AtomicInteger threadCounter = new AtomicInteger(N);
    public static final AtomicInteger queueCounter = new AtomicInteger(0);
    public static final Object poisonPill = new Object();
    public static volatile boolean cancel = false; // or use a final AomticBoolean instead
}

您的线程的轮询循环应如下所示(我假设您使用的是BlockingQueue

while(!ThreadCounter.cancel) {
    int threadCount = ThreadCounter.threadCounter.decrementAndGet(); // decrement before blocking
    if(threadCount == 0 && ThreadCounter.queueCounter.get() == 0) {
        ThreadCounter.cancel = true;
        queue.offer(ThreadCounter.poisonPill);
    } else {
        Object obj = queue.take();
        ThreadCounter.threadCounter.incrementAndGet(); // increment when the thread is no longer blocking
        ThreadCounter.queueCounter.decrementAndGet();
        if(obj == ThreadCounter.poisonPill) {
            queue.offer(obj); // send the poison pill back through the queue so the other threads can read it
            continue;
        }
    }
}

如果一个线程即将在BlockingQueue上阻塞,那么它会递减计数器;如果所有线程都在等待队列(意味着counter == 0),那么最后一个线程将cancel设置为true,然后通过队列发送毒丸以唤醒其他线程;每个线程看到毒丸,通过队列将其发回以唤醒剩余的线程,然后在看到cancel设置为true时退出循环。

编辑我通过添加queueCounter删除了数据竞争,该queueCounter.incrementAndGet()维护了队列中对象数量的计数(显然,您还需要添加{ {1}}调用您向队列添加对象的任何地方。这样做如下:如果threadCount == 0,但queueCount != 0,那么这意味着一个线程刚刚从队列中删除了一个项目但尚未调用threadCount.getAndIncrement,因此取消变量是设置为true。 threadCount.getAndIncrement调用先于queueCount.getAndDecrement调用,这一点很重要,否则您仍然会进行数据竞争。你调用queueCount.getAndIncrement的顺序无关紧要,因为你不会将它与threadCount.getAndDecrement的调用交织在一起(后者将在循环结束时被调用,前者将被调用在循环的开头)。

请注意,您不能仅使用queueCount来确定何时结束进程,因为线程可能仍处于活动状态而尚未在队列中放置任何数据 - 换句话说,{{1}将为零,但一旦线程完成当前迭代,它将为非零。

您可以改为通过队列发送取消线程(N-1)queueCount,而不是通过队列重复发送poisonPill。如果您使用不同的队列使用此方法,请务必谨慎,因为某些队列(例如亚马逊的简单队列服务)可能会返回相当于poisonPills方法的多个项目,在这种情况下您需要重复发送take以确保一切都关闭。

此外,代替使用poisonPill循环,您可以使用while(!cancel)循环并在循环检测到while(true)

时中断