2017-02-21 34 views
3

首先爲了防止標記問題重複由不喜歡閱讀到最後的傢伙我已閱讀Producer-Consumer Logging service with Unreliable way to shutdown 問題。但它不能完全回答與書中文本相矛盾的問題和答案。爲什麼LogWriter中的競爭條件會導致生產者阻塞? [實踐中的併發]

在書中提供了下面的代碼:

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"); 
} 

和評論這樣說:

關停日誌寫的另一種方法是設置標誌以防止進一步的消息 「關閉請求」提交了 ,如代碼清單7.14所示。然後消費者可以在收到關機請求通知後,排隊 , 寫出任何待處理消息並解除阻止日誌中的所有生產者 。但是,這種方法有競爭條件,使其不可靠 。日誌的執行是一個check-then-act序列: 生產者可以觀察到該服務尚未關閉 但仍然在關閉後對消息進行排隊,同樣有生產者可能在日誌中被阻止的風險,並且從未變得暢通無阻。 有一些技巧可以降低這種可能性(比如在宣佈排隊隊列之前讓消費者等待幾秒鐘),但這些並不會改變根本性問題,僅僅是可能導致失敗的可能性 。

這句話對我來說足夠難了。

我明白

if(!shutdownRequested) 
      queue.put(msg); 

不是原子和消息可以shutdowning之後添加到所述隊列。是的,這不是很準確,但我沒有看到問題。隊列就會流失,當隊列將空時,我們可以停止LoggerThread。尤其是我不明白爲什麼生產者可以封鎖

作者沒有提供完整的代碼,因此我無法理解所有細節。我相信這本書是由社區大多數人閱讀的,這個例子有詳細的解釋。

請用完整的代碼示例解釋。

回答

3

要理解的第一件事是,當請求關閉時,生產者需要停止接受任何更多的請求,並且消費者(在這種情況下爲LoggerThread)需要排空隊列。你在問題中出現的代碼只能說明故事的一面;當​​是true時,生產者拒絕任何進一步的請求。這個例子之後,筆者繼續說:

消費者可隨後在被通知 關機已要求,寫出任何未決的消息和 疏通阻塞在日誌中的任何生產商排在隊列

首先,queue.takeLoggerThread如您的問題所示將無限地阻止新消息在隊列中可用;然而,如果我們想關閉LoggerThread(優雅地),我們需要確保LoggerThread中的關機代碼有機會在​​爲真時執行,而不是無限制地被queue.take阻止。

當筆者說,消費者可以排出隊列中,他的意思是,LogWritter可以檢查​​,如果它是真的,它可以調用非阻塞drainTo方法排在隊列的當前內容在一個單獨的集合中,而不是調用queue.take(或者調用一個類似的非阻塞方法)。備選地,如果​​爲假,則LogWriter可以像往常一樣繼續呼叫queue.take

這種方法的真正問題在於log方法(由生產者調用)的實現方式。由於它不是原子的,因此多個線程可能會錯過​​的設置爲true。如果錯過此更新的線程數大於queueCAPACITY,會發生什麼情況。我們再來看看log的方法。 (爲說明起見添加了大括號):

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"); 
    } 
} 

步驟E所示,有可能對於多個生產者線程調用putLoggerThread完成排出隊列並退出瓦特直到1000th線程調用put應該沒有問題。真正的問題是當線程調用put時。它將阻塞,因爲隊列容量只有1000,並且LoggerThread可能不再活動或訂閱了queue.take方法。

+0

似乎相當牽強,期望1000線程在相同的兩行代碼之間等待。雖然我意識到如果理論上可行的話,它在技術上仍然是不正確的。 – shmosel

+0

我很感興趣,看看您是否可以演示關閉過程如何實際排空隊列*和所有阻止的生產者*。從我的測試中,'drainTo()'只消耗已排隊的項目。 – shmosel

+0

@shmosel我同意。這有點牽強,但我認爲你誤解了我的答案。 1000線程不會等待'put'。這是第1001線正在等待。對於'put'來阻止,你需要隊列中有1000個元素,並且第1001個塊試圖「放入」一個新元素。所有這一切,同時確保消費者不會爲第1001個元素騰出空間,只有當1001個線程嘗試在隊列中放入新元素時,消費者完成排隊並現在正在消失while循環。我沒有看到任何其他解釋該段的方式!你做? – CKing