FileWriter 與 BufferedWriter 指南
1. 概述
在本教程中,我們將了解兩個用於寫入檔案的基本 Java 類別之間的效能差異: FileWriter
和BufferedWriter
。雖然網路上的傳統觀點通常認為BufferedWriter
通常優於FileWriter
,但我們的目標是測試這個假設。
在了解了有關使用類別、類別的繼承及其內部實作的基本資訊之後,我們將使用 Java Microbenchmark Harness (JMH) 來測試BufferedWriter
是否確實具有優勢。
我們將在 Linux 上使用 JDK17 運行測試,但我們可以預期在任何作業系統上使用任何最新版本的 JDK 都會得到類似的結果。
2. 基本使用
FileWriter
使用預設緩衝區將文字寫入字元文件,該緩衝區的大小未在Javadoc中指定:
FileWriter writer = new FileWriter("testFile.txt");
writer.write("Hello, Baeldung!");
writer.close();
BufferedWriter
是一個替代選擇。它旨在包裝其他Writer
類,包括FileWriter
:
int BUFSIZE = 4194304; // 4MiB
BufferedWriter writer = new BufferedWriter(new FileWriter("testBufferedFile.txt"), BUFSIZE);
writer.write("Hello, Buffered Baeldung!");
writer.close();
在本例中,我們指定了 4MiB 緩衝區。但是,如果我們不設定緩衝區的大小,則Javadoc中不會指定其預設大小。
3、繼承
下面是一個 UML 圖,說明了FileWriter
和BufferedWriter
的繼承結構:
理解**FileWriter
和BufferedWriter
都擴充了Writer
是有幫助的,而FileWriter
的操作是基於OutputStreamWriter.
**不幸的是,繼承層次結構的分析和 Javadocs 都沒有告訴我們有關FileWriter
和BufferedWriter
的預設緩衝區大小的足夠信息,因此我們將檢查 JDK 原始碼以了解更多資訊。
4. 底層實現
查看FileWriter
的底層實現,我們可以看到它的預設緩衝區大小從JDK10到JDK18都是8192字節,在以後的版本中從512到8192可變。具體來說, FileWriter
擴展了OutputStreamWriter
,正如我們剛剛在 UML 圖中看到的那樣, OutputStreamWriter
使用StreamEncoder
,其程式碼在 JDK18 之前包含DEFAULT_BYTE_BUFFER_SIZE = 8192
,在更高版本中包含MAX_BYTE_BUFFER_CAPACITY = 8192
。
StreamEncoder
不是 JDK API 中的公用類別。它是sun.nio.cs
包中的一個內部類,用於在 Java 框架中處理字元流的編碼。
它的緩衝區大小允許FileWriter
透過最小化 I/O 操作數量來有效地處理資料。由於 Java 中的預設字元編碼通常為 UTF-8,因此在大多數情況下 8192 位元組將對應於大約 8192 個字元。儘管有這種有效的緩衝,但由於文件過時, FileWriter
仍然被認為沒有緩衝功能。
BufferedWriter
的預設緩衝區大小與FileWriter
相同。我們可以透過查看其原始碼進行驗證,從JDK10到JDK18,原始碼中包含defaultCharBufferSize = 8192
,之後的版本DEFAULT_MAX_BUFFER_SIZE = 8192
。然而, BufferedWriter
允許我們指定不同的緩衝區大小,正如我們在前面的範例中看到的那樣。
5. 效能比較
這裡我們將FileWriter
和BufferedWriter
與 JMH 做比較。如果我們想在我們的機器上複製測試,並且使用 Maven,我們需要在pom.xml
上設定 JMH 依賴項,將 JMH 註解處理器新增至 Maven 編譯器插件配置中,並確保所有所需的類別和資源在執行期間可用。我們的 JMH 教程的入門部分涵蓋了這些要點。
5.1.磁碟寫同步
要使用JHM進行磁碟寫入基準測試,必須透過停用作業系統快取來實現磁碟操作的完全同步。此步驟至關重要,因為非同步磁碟寫入會顯著影響 I/O 操作測量的準確性。預設情況下,作業系統將頻繁存取的資料儲存在記憶體中,從而減少實際磁碟寫入次數並使基準測試結果無效。
在Linux系統上,我們可以使用mount
的sync
選項重新掛載檔案系統以停用快取並確保所有寫入操作立即同步到磁碟:
$ sudo mount -o remount,sync /path/to/mount
同樣, macOS mount
有一個sync
選項,可確保檔案系統的所有 I/O 都是同步的。
在 Windows 上,我們開啟Device Manager
並展開Drives
部分。然後,右鍵單擊要設定的驅動器,選擇Properties,
然後導航到Policies
標籤。最後,我們停用Enable write caching on the device
選項。
5.2.我們的測試
我們的程式碼測量了FileWriter
和BufferedWriter
在各種寫入條件下的效能。我們執行多個基準測試來測試對benchmark.txt
檔案的單次寫入和重複寫入(10、1000、10000 和 100000 次)。
我們使用JMH 特定的註解來配置基準參數,例如@Benchmark
、 @State
、 @BenchmarkMode
等,以設定範圍、模式、預熱迭代、測量迭代和分叉設定。
main
方法透過在執行 JMH 基準測試套件之前刪除任何現有benchmark.txt
檔案並調整類別路徑來設定環境:
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 10, timeUnit = TimeUnit.SECONDS)
@Fork(1)
public class BenchmarkWriters {
private static final Logger log = LoggerFactory.getLogger(BenchmarkWriters.class);
private static final String FILE_PATH = "benchmark.txt";
private static final String CONTENT = "This is a test line.";
private static final int BUFSIZE = 4194304; // 4MiB
@Benchmark
public void fileWriter1Write() {
try (FileWriter writer = new FileWriter(FILE_PATH, true)) {
writer.write(CONTENT);
writer.close();
} catch (IOException e) {
log.error("Error in FileWriter 1 write", e);
}
}
@Benchmark
public void bufferedWriter1Write() {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_PATH, true), BUFSIZE)) {
writer.write(CONTENT);
writer.close();
} catch (IOException e) {
log.error("Error in BufferedWriter 1 write", e);
}
}
@Benchmark
public void fileWriter10Writes() {
try (FileWriter writer = new FileWriter(FILE_PATH, true)) {
for (int i = 0; i < 10; i++) {
writer.write(CONTENT);
}
writer.close();
} catch (IOException e) {
log.error("Error in FileWriter 10 writes", e);
}
}
@Benchmark
public void bufferedWriter10Writes() {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_PATH, true), BUFSIZE)) {
for (int i = 0; i < 10; i++) {
writer.write(CONTENT);
}
writer.close();
} catch (IOException e) {
log.error("Error in BufferedWriter 10 writes", e);
}
}
@Benchmark
public void fileWriter1000Writes() {
[...]
}
@Benchmark
public void bufferedWriter1000Writes() {
[...]
}
@Benchmark
public void fileWriter10000Writes() {
[...]
}
@Benchmark
public void bufferedWriter10000Writes() {
[...]
}
@Benchmark
public void fileWriter100000Writes() {
[...]
}
@Benchmark
public void bufferedWriter100000Writes() {
[...]
}
[...]
}
在這些測試中,每個基準測試方法都會獨立開啟和關閉檔案編寫器。 @Fork(1)
註解表示僅使用一個 fork,因此同一基準方法不會出現多個並行執行。該程式碼沒有明確創建或管理線程,因此所有寫入都在基準測試的主線程中完成。
所有這些意味著寫入確實是順序的而不是並發的,這是獲得有效測量所必需的。
5.3.結果
這些是程式碼中指定BufferedWriter
緩衝區大小為 4MiB 時的結果:
Benchmark Mode Cnt Score Error Units
BenchmarkWriters.bufferedWriter100000Writes avgt 10 9170.583 ± 245.916 ms/op
BenchmarkWriters.bufferedWriter10000Writes avgt 10 918.662 ± 15.105 ms/op
BenchmarkWriters.bufferedWriter1000Writes avgt 10 114.261 ± 2.966 ms/op
BenchmarkWriters.bufferedWriter10Writes avgt 10 37.999 ± 1.571 ms/op
BenchmarkWriters.bufferedWriter1Write avgt 10 37.968 ± 2.219 ms/op
BenchmarkWriters.fileWriter100000Writes avgt 10 9253.935 ± 261.032 ms/op
BenchmarkWriters.fileWriter10000Writes avgt 10 951.684 ± 41.391 ms/op
BenchmarkWriters.fileWriter1000Writes avgt 10 114.610 ± 4.366 ms/op
BenchmarkWriters.fileWriter10Writes avgt 10 37.761 ± 1.836 ms/op
BenchmarkWriters.fileWriter1Write avgt 10 37.912 ± 2.080 ms/op
相反,這些是沒有為BufferedWriter
指定緩衝區值的結果,即使用其預設緩衝區:
Benchmark Mode Cnt Score Error Units
BenchmarkWriters.bufferedWriter100000Writes avgt 10 9117.021 ± 143.096 ms/op
BenchmarkWriters.bufferedWriter10000Writes avgt 10 931.994 ± 34.986 ms/op
BenchmarkWriters.bufferedWriter1000Writes avgt 10 113.186 ± 2.076 ms/op
BenchmarkWriters.bufferedWriter10Writes avgt 10 40.038 ± 2.042 ms/op
BenchmarkWriters.bufferedWriter1Write avgt 10 38.891 ± 0.684 ms/op
BenchmarkWriters.fileWriter100000Writes avgt 10 9261.613 ± 305.692 ms/op
BenchmarkWriters.fileWriter10000Writes avgt 10 932.001 ± 26.676 ms/op
BenchmarkWriters.fileWriter1000Writes avgt 10 114.209 ± 5.988 ms/op
BenchmarkWriters.fileWriter10Writes avgt 10 38.205 ± 1.361 ms/op
BenchmarkWriters.fileWriter1Write avgt 10 37.490 ± 2.137 ms/op
本質上,這些結果表明FileWriter
和BufferedWriter
的性能在所有測試條件下幾乎相同。此外,為BufferedWriter
指定比預設緩衝區更大的緩衝區並不會帶來任何好處。
六,結論
在本文中,我們探討了使用 JHM 的FileWriter
和BufferedWriter
之間的效能差異。我們首先查看它們的基本用法和繼承結構。從 JDK10 到 JDK18,這兩個類別的預設緩衝區大小均為 8192 字節,在更高版本中從 512 到 8192 位元組可變。
我們運行基準測試來比較它們在各種條件下的效能,透過停用作業系統快取來確保準確的測量。測試包括使用BufferedWriter
的預設和指定 4MiB 緩衝區進行單次和重複寫入。
我們的結果顯示FileWriter
和BufferedWriter
在所有場景中都具有幾乎相同的效能。此外,增加BufferedWriter
的緩衝區大小並不能顯著提高效能。
與往常一樣,完整的源代碼可以在 GitHub 上取得。