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

徹底轉變流(一):從只能寫入的數據源高效讀取數據的三種方案對比

     編輯:Java 相關知識

問題起源

Java I/O 框架雖強大,但缺乏靈活性:壓縮流只能寫入壓縮格式、無法讀取;第三方模塊常只支援寫入輸出流;XML 序列化通常只寫不讀。核心問題:有一個遞增寫入輸出流的數據源,需要一個輸入流透明讀取這些數據。本文為兩部分系列第一篇,分析三種解決方案並確立最佳框架。

I/O 流基礎回顧

  • OutputStream:寫入數據,可鏈接設備或另一輸出流(過濾器)。
  • InputStream:讀取數據,同樣可鏈接。
  • GZIPOutputStream 是輸出流過濾器,壓縮寫入數據。需求是輸入流過濾器:讀取數據、壓縮、允許讀取結果。

java.nio 主要針對 OS I/O 資源高效訪問,不直接解決此問題。

方案一:蠻力解法

將所有數據讀入內存緩衝區(ByteArrayOutputStream)經轉換器處理,再用 ByteArrayInputStream 讀取。

public static InputStream bruteForceCompress(InputStream in) throws IOException {
    ByteArrayOutputStream sink = new ByteArrayOutputStream();
    OutputStream out = new GZIPOutputStream(sink);
    io(in, out); // 通用複製方法
    out.close();
    byte[] buffer = sink.toByteArray(); // 複製
    return new ByteArrayInputStream(buffer); // 再次複製
}

缺點:全量數據駐留內存,大文件浪費資源。效率上經歷四次緩衝區複製(讀入→壓縮→擴容複製→toByteArray複製→ByteArrayInputStream複製),ByteArrayOutputStream 擴容平均每字節複製兩次。

方案二:管道流解法

用 PipedOutputStream/PipedInputStream 連接兩線程:一線程壓縮寫入,另一線程讀取。

private static InputStream pipedCompress(final InputStream in) throws IOException {
    PipedInputStream source = new PipedInputStream();
    final OutputStream out = new GZIPOutputStream(new PipedOutputStream(source));
    new Thread() { public void run() { try { Streams.io(in, out); out.close(); } catch (IOException ex) { ex.printStackTrace(); } } }.start();
    return source;
}

理論優勢:雙線程可受益 SMP/SMT,僅兩次緩衝區複製,固定緩衝區流式處理無需全量內存。
實際災難:同步導致劇烈鎖爭用、內部緩衝區太小、持久共享緩衝區阻礙 CPU 緩存親和性、異常處理極難(無法將異常傳遞給讀端)。性能極差,不實用。

方案三:工程解法——OutputEngine 框架

設計一個框架:數據源遞增寫入 OutputStream,框架提供 InputStream 讀取。關鍵接口 OutputEngine:

public interface OutputEngine {
    void initialize(OutputStream out) throws IOException;
    void execute() throws IOException; // 遞增寫入一塊數據
    void finish() throws IOException;  // 清理資源
}

核心實現:IOStreamEngine

通過輸入流與 OutputStreamFactory 構造,execute() 讀取一塊數據寫入工廠產生的過濾輸出流。GZIPOutputStreamFactory 示例:

public class GZIPOutputStreamFactory implements IOStreamEngine.OutputStreamFactory {
    public OutputStream getOutputStream(OutputStream out) { return new GZIPOutputStream(out); }
}

輸出引擎輸入流:OutputEngineInputStream

內部維護讀緩衝區,read() 發現緩衝區空時調用 engine.execute() 重新填充。內部輸出流 OutputStreamImpl 接收引擎寫入的數據,直接複製到讀緩衝區(緩衝區不足時翻倍)。引擎關閉輸出流時設置 eof 標記。

關鍵 writeImpl:

private void writeImpl(byte[] data, int offset, int length) {
    if (index >= limit) index = limit = 0; // 緩衝區用完重置
    if (limit + length > capacity) { /* 翻倍擴容 */ }
    System.arraycopy(data, offset, buffer, limit, length);
    limit += length;
}

應用框架解決壓縮讀取

private static InputStream engineCompress(InputStream in) throws IOException {
    return new OutputEngineInputStream(new IOStreamEngine(in, new GZIPOutputStreamFactory()));
}

代碼清晰,良好設計產生可維護代碼。

性能測試(同環境)

方案15KB15MB
管道流23 ms22100 ms
蠻力0.35 ms745 ms
工程方案0.16 ms73 ms

工程方案顯著優於標準 API 兩種方案。若輸出引擎承諾寫入後不修改數組,可進一步降至一次複製,但極少見。

擴展應用:讀取編碼字符流與序列化 DOM

ReaderWriterEngine

從 Reader 讀取字符,經 OutputStreamWriter 編碼寫入輸出流。Java 1.4 前唯一有效方案,1.4+ java.nio.charset 提供無流編碼但不滿足基於流需求。

DOMSerializerEngine

遍歷 DOM 節點迭代器,序列化寫入 OutputStreamWriter,可用於部分 DOM 文檔重新解析驗證。

結語

提供的框架高效讀取只能寫入輸出流的系統產生的數據(壓縮、編碼、序列化)。任何寫入 ByteArrayOutputStream 後續處理的應用皆可受益。下一篇將重寫字節數組流與管道流,展示 API 約定稍微放寬即可帶來百倍性能提升。靈感源於洗碗時的思考——跳出代碼审視問題全貌,往往能得出更清晰、高效、可維護的方案。

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