BufferedOutputStream 繼承自 FilterOutputStream,內部維護一個位元組陣列緩衝區(預設 8192 位元組)。它攔截寫入操作,將資料先累積在記憶體緩衝區,待緩衝區滿或显式 flush() 時,一次性寫入底層 OutputStream,大幅減少系統呼叫開銷,提升寫入吞吐量。核心特性:同步方法、自動滿刷新、大資料繞過緩衝、關閉前自動 flush。
// 建構子
BufferedOutputStream(OutputStream out) // 預設 8KB 緩衝
BufferedOutputStream(OutputStream out, int size) // 自訂緩衝區大小
// 核心寫入(同步)
synchronized void write(int b) // 單位元組
synchronized void write(byte[] b, int off, int len) // 位元組陣列片段
// 緩衝控制
synchronized void flush() // 強制刷新緩衝區至底層
synchronized void close() // 刷新後關閉底層流
public class BufferedOutputStream extends FilterOutputStream {
protected byte[] buf; // 位元組緩衝區
protected int count; // 緩衝區有效資料長度
public BufferedOutputStream(OutputStream out) {
this(out, 8192);
}
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) throw new IllegalArgumentException("Buffer size <= 0");
buf = new byte[size];
}
// 刷新緩衝區至底層流
private void flushBuffer() throws IOException {
if (count > 0) {
out.write(buf, 0, count);
count = 0;
}
}
// 單位元組寫入:緩衝區滿則自動 flush
public synchronized void write(int b) throws IOException {
if (count >= buf.length) flushBuffer();
buf[count++] = (byte) b;
}
// 陣列寫入:三種策略
public synchronized void write(byte[] b, int off, int len) throws IOException {
if (len >= buf.length) { // 1. 大資料 ≥ 緩衝區:直接寫入底層(繞過緩衝)
flushBuffer();
out.write(b, off, len);
return;
}
if (len > buf.length - count) { // 2. 剩餘空間不足:先 flush 再寫入緩衝
flushBuffer();
}
// 3. 正常寫入緩衝區
System.arraycopy(b, off, buf, count, len);
count += len;
}
public synchronized void flush() throws IOException {
flushBuffer();
out.flush();
}
}
緩衝區大小 = 16 bytes, 當前 count = 10 情況 A:write(byte[20]) → len(20) >= buf.length(16) → flushBuffer() → out.write(20 bytes) [繞過緩衝,零複製] 情況 B:write(byte[10]) → len(10) > buf.length-count(6) → flushBuffer() → System.arraycopy(10 bytes to buf) [先清空,再緩衝] 情況 C:write(byte[5]) → len(5) <= buf.length-count(6) → System.arraycopy(5 bytes to buf) [直接緩衝,count=15]
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Random;
public class BufferedOutputStreamDemo {
public static void main(String[] args) {
demoFlushBehavior();
benchmarkBufferSizes();
demoLargeDataBypass();
}
// 演示 flush 行為:緩衝區未滿時資料不落盤
private static void demoFlushBehavior() throws IOException {
Path path = Path.of("flush_demo.txt");
Files.deleteIfExists(path);
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(path.toFile()), 16)) { // 16 bytes 緩衝
bos.write("ABCDEFGHIJ".getBytes()); // 10 bytes
bos.write('\n'); // 1 byte, 總計 11 < 16
// 此時檔案應為空(未 flush)
System.out.println("寫入 11 bytes 後檔案大小: " + Files.size(path)); // 0
bos.flush(); // 手動刷新
System.out.println("flush() 後檔案大小: " + Files.size(path)); // 11
bos.write("KLMNO".getBytes()); // 5 bytes, 總計 5
// 程式正常結束,close() 會自動 flush
}
System.out.println("close() 後最終大小: " + Files.size(path)); // 16
}
// 緩衝區大小對寫入效能影響測試
private static void benchmarkBufferSizes() {
int dataSize = 10_000_000; // 10 MB
byte[] data = new byte[dataSize];
new Random().nextBytes(data);
int[] bufferSizes = {512, 4096, 8192, 65536, 1048576}; // 0.5KB ~ 1MB
for (int bufSize : bufferSizes) {
Path path = Path.of("bench_" + bufSize + ".tmp");
long start = System.nanoTime();
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(path.toFile()), bufSize)) {
bos.write(data);
} catch (IOException e) { e.printStackTrace(); continue; }
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
double throughputMBs = (dataSize / 1_000_000.0) / (elapsedMs / 1000.0);
System.out.printf("緩衝區: %7d bytes | 耗時: %4d ms | 吞吐: %.2f MB/s%n",
bufSize, elapsedMs, throughputMBs);
try { Files.deleteIfExists(path); } catch (IOException ignored) {}
}
}
// 大資料繞過緩衝區直接寫入驗證
private static void demoLargeDataBypass() throws IOException {
Path path = Path.of("bypass_demo.txt");
Files.deleteIfExists(path);
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(path.toFile()), 16)) { // 16 bytes 緩衝
// 寫入 20 bytes (> 16),應直接繞過緩衝寫入底層
byte[] largeData = new byte[20];
for (int i = 0; i < 20; i++) largeData[i] = (byte) ('A' + i);
bos.write(largeData);
// 此時檔案應已有 20 bytes(未 close、未 flush)
System.out.println("寫入 20 bytes (>緩衝區) 後檔案大小: " + Files.size(path)); // 20
bos.write("SMALL".getBytes()); // 5 bytes,進緩衝
System.out.println("再寫 5 bytes 後檔案大小: " + Files.size(path)); // 20 (緩衝未滿)
}
System.out.println("close() 後最終大小: " + Files.size(path)); // 25
}
}
| 場景 | 建議緩衝區大小 | 說明 |
|---|---|---|
| 一般檔案寫入 | 8 KB - 64 KB | 平衡記憶體與系統呼叫,8KB 為經驗預設值 |
| 大檔案序列化/備份 | 128 KB - 1 MB | 減少系統呼叫頻率,利用磁碟順序寫入特性 |
| 網路輸出 (Socket) | 8 KB - 16 KB | 配合 TCP 封包大小,避免過大緩衝增加延遲 |
| 記憶體受限環境 | 1 KB - 4 KB | 嵌入式、容器限制記憶體時 |
| 已知寫入塊大小 | 塊大小整數倍 | 如 4KB 頁面、64KB Stripe,對齊減少複製 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| BufferedOutputStream | 標準 API、自動管理、相容性好 | 同步鎖開銷、單緩衝區 | 一般檔案、網路輸出 |
| ByteBuffer + Channel | 直接緩衝、非堆外記憶體、支援散佈/聚集 | API 較複雜、需手動管理 | 高效能、大檔案、NIO 應用 |
| MappedByteBuffer | 記憶體映射、OS 頁面快取、極高吞吐 | 檔案大小受限、關閉時機複雜 | 大檔案隨機讀寫、日誌追加 |
| 自定義雙緩衝/環狀緩衝 | 生產者消費者解耦、非阻塞寫入 | 實現複雜、需處理背壓 | 高併發非同步寫入 |
close() 內部呼叫 flush(),資料會落盤。但異常中斷時需靠 try-with-resources 保證。new BufferedOutputStream(new BufferedOutputStream(...)) 雙重緩衝無意義且增加開銷。
// Java 7+ 標準開啟模式
try (BufferedOutputStream bos = new BufferedOutputStream(
Files.newOutputStream(path,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING),
65536)) {
bos.write(data);
}
// 或使用 NIO Channel 直接寫入(無緩衝開銷)
try (FileChannel channel = FileChannel.open(path,
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
channel.write(ByteBuffer.wrap(data));
}
BufferedOutputStream 是 Java 位元組寫入效能的基石,理解其三種寫入策略、緩衝區大小權衡與 flush/close 語義,能針對不同場景調優出最佳吞吐與延遲平衡。