首先,为了防止标记问题被不喜欢阅读的人重复,我已经阅读了Producer-Consumer Logging service with Unreliable way to shutdown 个问题。但它没有完全回答问题和答案与书中的文字相矛盾。
在书中提供以下代码:
public class LogWriter {
private final BlockingQueue<String> queue;
private final LoggerThread logger;
private static final int CAPACITY = 1000;
public LogWriter(Writer writer) {
this.queue = new LinkedBlockingQueue<String>(CAPACITY);
this.logger = new LoggerThread(writer);
}
public void start() {
logger.start();
}
public void log(String msg) throws InterruptedException {
queue.put(msg);
}
private class LoggerThread extends Thread {
private final PrintWriter writer;
public LoggerThread(Writer writer) {
this.writer = new PrintWriter(writer, true); // autoflush
}
public void run() {
try {
while (true)
writer.println(queue.take());
} catch (InterruptedException ignored) {
} finally {
writer.close();
}
}
}
}
现在我们应该了解如何停止这个过程。我们应该停止记录,但不应该跳过已经提交的消息。
作者研究方法:
public void log(String msg) throws InterruptedException {
if(!shutdownRequested)
queue.put(msg);
else
throw new IllegalArgumentException("logger is shut down");
}
并像这样评论:
关闭LogWriter的另一种方法是设置一个 “shutdown requested”标志以防止进一步的消息 提交,如代码清单7.14所示。然后消费者可以流失 被通知已请求关闭时的队列, 写出任何待处理的消息并解除阻止任何生产者的阻止 在日志中。然而,这种方法具有竞争条件 不可靠的。 log的实现是一个check-then-act序列: 生产者可以观察到该服务尚未关闭 但是在关机后仍然排队消息,再次冒着风险 生产者可能会在日志中被阻止,永远不会被解除阻止。 有一些技巧可以降低这种可能性(比如拥有 消费者在宣布队列耗尽之前等待几秒钟,但是 这些不会改变根本问题,仅仅是可能性 这将导致失败。
这句话对我来说足够困难。
我理解
if(!shutdownRequested)
queue.put(msg);
不是原子的,并且可以在关闭后将消息添加到队列中。是的,它不是很准确,但我没有看到问题。队列刚刚耗尽,当队列为空时我们可以停止LoggerThread。特别是我不明白为什么可以阻止制作人。
作者没有提供完整的代码,因此我无法理解所有细节。我相信这本书是由社区大多数人阅读的,这个例子有详细的解释。
请用完整的代码示例解释。
答案 0 :(得分:3)
首先要理解的是,当请求关闭时,生产者需要停止接受任何更多请求,而消费者(在这种情况下为LoggerThread
)需要耗尽队列。您在问题中提供的代码只展示了故事的一个方面;生产者在shutdownRequested
为true
时拒绝任何进一步的请求。在这个例子之后,作者继续说:
然后,消费者可以在得到通知后排空队列 已请求关闭,写出任何待处理的消息和 解锁阻止日志中的任何生产者
首先,您问题中显示的queue.take
中的LoggerThread
将无限制地阻止队列中可用的新消息;但是,如果我们想要关闭LoggerThread
(优雅地),我们需要确保LoggerThread
中的关闭代码有机会在shutdownRequested
为真时执行,而不是无限制地被阻止queue.take
。
当作者说消费者可以消耗队列时,他的意思是LogWritter
可以检查shutdownRequested
,如果它是真的,它可以调用非阻塞drainTo方法,用于在单独的集合中排空队列的当前内容,而不是调用queue.take
(或者调用类似的非阻塞方法)。替代方案,如果shutdownRequested
为假,LogWriter
可以照常调用queue.take
。
这种方法的真正问题在于实现log
方法(由生产者调用)的方式。由于它不是原子的,因此多个线程可能会错过shutdownRequested
的设置为true。如果错过此更新的线程数大于CAPACITY
的{{1}},会发生什么情况。让我们再看看queue
方法。 (为了便于解释,添加了大括号):
log
如步骤E 所示,当public void log(String msg) throws InterruptedException {
if(!shutdownRequested) {//A. 1001 threads see shutdownRequested as false and pass the if condition.
//B. At this point, shutdownRequested is set to true by client code
//C. Meanwhile, the LoggerThread which is the consumer sees that shutdownRequested is true and calls
//queue.drainTo to drain all existing messages in the queue instead of `queue.take`.
//D. Producers insert the new message into the queue.
queue.put(msg);//Step E
} else
throw new IllegalArgumentException("logger is shut down");
}
}
完成排空队列并退出w时,多个生产者线程可能会调用put
。在 1000th 线程调用LoggerThread
之前应该没有问题。真正的问题是当 1001th 线程调用put
时。它将阻塞,因为队列容量仅为1000且put
可能不再存在或订阅LoggerThread
方法。