首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java IO 緩衝流:BufferedOutputStream 位元組緩衝輸出流完整指南

     編輯:Java 相關知識

核心定位:為位元組輸出流添加緩衝能力

BufferedOutputStream 繼承自 FilterOutputStream,內部維護一個位元組陣列緩衝區(預設 8192 位元組)。它攔截寫入操作,將資料先累積在記憶體緩衝區,待緩衝區滿或显式 flush() 時,一次性寫入底層 OutputStream,大幅減少系統呼叫開銷,提升寫入吞吐量。核心特性:同步方法、自動滿刷新、大資料繞過緩衝、關閉前自動 flush。

關鍵 API


// 建構子
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 vs 其他緩衝策略

方案優點缺點適用場景
BufferedOutputStream標準 API、自動管理、相容性好同步鎖開銷、單緩衝區一般檔案、網路輸出
ByteBuffer + Channel直接緩衝、非堆外記憶體、支援散佈/聚集API 較複雜、需手動管理高效能、大檔案、NIO 應用
MappedByteBuffer記憶體映射、OS 頁面快取、極高吞吐檔案大小受限、關閉時機複雜大檔案隨機讀寫、日誌追加
自定義雙緩衝/環狀緩衝生產者消費者解耦、非阻塞寫入實現複雜、需處理背壓高併發非同步寫入

常見誤區與避坑

  1. 以為 close() 不會 flush:close() 內部呼叫 flush(),資料會落盤。但異常中斷時需靠 try-with-resources 保證。
  2. 頻繁小量 write + 手動 flush:抵消緩衝優勢,應批次累積或依賴自動滿刷新。
  3. 緩衝區設太大導致延遲:互動式、即時日誌場景,大緩衝會導致資料遲遲不可見。
  4. 多執行緒共享同一實例:方法雖同步,但高併發下鎖競爭嚴重,建議每執行緒獨立實例或使用非同步框架。
  5. 包裝已緩衝的流:new BufferedOutputStream(new BufferedOutputStream(...)) 雙重緩衝無意義且增加開銷。

現代替代:Files.newOutputStream + 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 語義,能針對不同場景調優出最佳吞吐與延遲平衡。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved