轉自:https://gpp.tkchu.me/double-buffer.html,chatgpt,https://blog.51cto.com/u_15214399/4914060
1.介紹
定義緩衝類封裝了緩衝:一段可改變的狀態。 這個緩衝被增量地修改,但我們想要外部的程式碼將修改視為單一的原子操作。 為了實現這點,類儲存了兩個緩衝的例項:下一緩衝和當前緩衝。 以下情況都滿足時,使用這個模式就很恰當:
- 我們需要維護一些被增量修改的狀態。
- 在修改到一半的時候,狀態可能會被外部請求。
- 我們想要防止請求狀態的外部程式碼知道內部的工作方式。
- 我們想要讀取狀態,而且不想等著修改完成。
關鍵點:
- 在狀態被修改後,雙緩衝需要一個swap步驟。 交換緩衝區的指標或者引用,速度快。 不管緩衝區有多大,交換都只需賦值一對指標。
- 這個模式的另一個結果是增加了記憶體的使用。 正如其名,這個模式需要你在記憶體中一直保留兩個狀態的複製。 在記憶體受限的裝置上,你可能要付出慘痛的代價。 如果你不能接受使用兩份記憶體,你需要使用別的方法保證狀態在修改時不會被請求。
雙緩衝解決的核心問題是狀態有可能在被修改的同時被請求。
2.例子
應用程式向磁碟寫入日誌,引入雙緩衝區機制,一個緩衝區儲存應用程式端傳送的日誌,按照時間順序依次儲存;另一個緩衝區負責向低層磁碟傳送寫檔案請求。雙緩衝區的奇妙之處就在於,兩個緩衝區的交換,是透過交換指標來實現的,非常的高效。
// 緩衝區1: 負責接收應用程式發來的日誌 LinkedList<String> currentBuffer = new LinkedList<>(); // 緩衝區2: 負責將資料同步到磁碟 LinkedList<String> syncBuffer = new LinkedList<>(); // 應用程式寫日誌,寫入第一個緩衝區 public void log(String content) { // 加鎖保證第一個緩衝區 synchronized(this) { // 將log寫入記憶體緩衝中,這裡不會直接刷入磁碟檔案 currentBuffer.add(content); } // 將緩衝區中的內容刷到磁碟 logSync(); } // 第二緩衝區,向磁碟寫日誌,並在寫入後交換緩衝區指標 private void logSync() { synchronized(this) { // 當前在刷記憶體緩衝到磁碟中去 if (isSyncRunning) { // 判斷是否第二個緩衝區還在刷 while (isSyncRunning) { try { // 釋放鎖,即允許第一個緩衝區繼續接收日誌快取, 然後等待被喚醒 wait(2000); } catch (Exception e) { e.printStackTrace(); } } // 此時沒有人在寫磁碟 } // 交換緩衝區指標 setReadyToSync(); // 設定當前正在同步到磁碟的標誌位 isSyncRunning = true; } // 刷磁碟,效能最低,不能加鎖 logBuffer.flush(); synchronized(this) { // 同步完磁碟之後,將標誌位復位 isSyncRunning = false; // 喚醒其他等待刷磁碟的執行緒 notifyAll(); } } public void setReadyToSync() { LinkedList<String> tmp = currentBuffer; currentBuffer = syncBuffer; syncBuffer = tmp; }
兩個緩衝區各自處理,互不干擾
兩個緩衝區很好的解決了應用程式的“快速、多執行緒”與IO操作的“緩慢,單執行緒”的矛盾。應該說,引入雙緩衝區是一個顯而易見的方式。