为什么LogWriter中的竞争条件会导致生产者阻塞? [实践中的并发]

时间:2017-02-21 09:24:27

标签: java multithreading concurrency shutdown blockingqueue

首先,为了防止标记问题被不喜欢阅读的人重复,我已经阅读了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。特别是我不明白为什么可以阻止制作人

作者没有提供完整的代码,因此我无法理解所有细节。我相信这本书是由社区大多数人阅读的,这个例子有详细的解释。

请用完整的代码示例解释。

1 个答案:

答案 0 :(得分:3)

首先要理解的是,当请求关闭时,生产者需要停止接受任何更多请求,而消费者(在这种情况下为LoggerThread)需要耗尽队列。您在问题中提供的代码只展示了故事的一个方面;生产者在shutdownRequestedtrue时拒绝任何进一步的请求。在这个例子之后,作者继续说:

  

然后,消费者可以在得到通知后排空队列   已请求关闭,写出任何待处理的消息和   解锁阻止日志中的任何生产者

首先,您问题中显示的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方法。