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

Java RandomAccessFile 效能優化:自製 BufferedRandomAccessFile 實現高效 I/O

     編輯:Java 相關知識

前言:RandomAccessFile 的效能瓶頸

在 JDK 1.3 時代,RandomAccessFile 是實現文件隨機讀寫的核心類。然而,其 I/O 效能與其他語言的同類功能相比差距巨大,嚴重拖慢程序運行速度。本文將深入剖析其源碼,找出關鍵瓶頸,並逐步構建一個「性價比」俱佳的緩衝隨機訪問類 BufferedRandomAccessFile。

基準測試:驚人的 32 倍差距

先做一個基礎測試:逐字節複製一個 12MB 文件(涉及讀與寫)。

讀取方式寫入方式耗時(秒)
RandomAccessFileRandomAccessFile95.848
BufferedInputStream + DataInputStreamBufferedOutputStream + DataOutputStream2.935

兩者相差約 32 倍,RandomAccessFile 確實太慢。接著看看雙方核心部分的源碼,找出原因。

源碼剖析:為何 RandomAccessFile 這麼慢?

1. RandomAccessFile:無緩衝,逐字節直擊磁盤

public final byte readByte() throws IOException {
    int ch = this.read(); // native 方法,每次調用都觸發磁盤 I/O
    if (ch < 0) throw new EOFException();
    return (byte)(ch);
}
public native int read() throws IOException;

public final void writeByte(int v) throws IOException {
    write(v); // native 方法,每次調用都觸發磁盤 I/O
}
public native void write(int b) throws IOException;

可見,RandomAccessFile 每讀/寫一個字節就需對磁盤進行一次 I/O 操作。

2. BufferedInputStream / BufferedOutputStream:記憶體緩衝區大幅減少磁盤 I/O

以 BufferedInputStream 為例,內部維護一個 byte buf[]。讀取時,若數據在緩衝區中,直接從記憶體讀取;否則從磁盤填充緩衝區再讀取。絕大多數操作是對記憶體 buf[] 的操作。

public synchronized int read() throws IOException {
    ensureOpen();
    if (pos >= count) { fill(); ... }
    return buf[pos++] & 0xff; // 直接從記憶體讀取
}

BufferedOutputStream 同理,寫入先進緩衝區,滿了才 flushBuffer() 寫入磁盤。

3. 結論:記憶體存取納秒級,磁盤毫秒級,差距百萬倍

即便對記憶體操作上萬次,耗時也遠小於磁盤一次 I/O。緩衝機制正是通過增加記憶體存取,減少磁盤 I/O 開銷,提升 32 倍效能。

構建 BufferedRandomAccessFile:為隨機訪問加上緩衝

隨機訪問類實現 DataInput/DataOutput 接口,無法直接繼承 FilterInputStream。需自行實現緩衝邏輯。

2.1 開闢共用緩衝區 BUF[默認 1024 字節]

2.2 實現讀緩衝

核心邏輯:

  1. 欲讀文件 POS 位置的一個字節。
  2. 查 BUF 中是否存在?若有,直接從 BUF 讀取並移動指針。
  3. 若無,則 BUF 重新定位到該位置,填充鄰近 BUFSIZE 字節,回到步驟 2。

關鍵代碼片段(簡化版):

public class BufferedRandomAccessFile extends RandomAccessFile {
    private byte[] buf = new byte[1024];
    private long bufStartPos, bufEndPos, curPos, fileEndPos;
    private boolean bufDirty = false;
    private int bufUsedSize;

    public byte read(long pos) throws IOException {
        if (pos < bufStartPos || pos > bufEndPos) {
            flushBuf();
            seek(pos);
            if (pos < bufStartPos || pos > bufEndPos) throw new IOException();
        }
        curPos = pos;
        return buf[(int)(pos - bufStartPos)];
    }

    private void flushBuf() throws IOException {
        if (bufDirty) {
            if (super.getFilePointer() != bufStartPos) super.seek(bufStartPos);
            super.write(buf, 0, bufUsedSize);
            bufDirty = false;
        }
    }

    public void seek(long pos) throws IOException {
        if (pos < bufStartPos || pos > bufEndPos) {
            flushBuf();
            if (pos >= 0 && pos <= fileEndPos && fileEndPos != 0) {
                bufStartPos = pos & bufMask; // 優化後的對齊計算
                bufUsedSize = fillBuf();
            } else if ((pos == 0 && fileEndPos == 0) || (pos == fileEndPos + 1)) {
                bufStartPos = pos;
                bufUsedSize = 0;
            }
            bufEndPos = bufStartPos + buf.length - 1;
        }
        curPos = pos;
    }

    private int fillBuf() throws IOException {
        super.seek(bufStartPos);
        bufDirty = false;
        return super.read(buf);
    }
}

測試讀緩衝效果

讀取方式寫入方式耗時(秒)
RandomAccessFileRandomAccessFile95.848
BufferedRandomAccessFileBufferedOutputStream + DataOutputStream2.813
BufferedInputStream + DataInputStreamBufferedOutputStream + DataOutputStream2.935

讀取速度明顯提升,與 BufferedInputStream 平起平坐。

2.3 實現寫緩衝

核心邏輯:

  1. 欲寫文件 POS 位置的一個字節。
  2. 查 BUF 中是否有該映射?若有,直接向 BUF 寫入,標記髒標誌。
  3. 若無,則 BUF 重新定位到該位置,填充鄰近數據,再寫入。

關鍵代碼(簡化版):

public boolean write(byte bw, long pos) throws IOException {
    if (pos >= bufStartPos && pos <= bufEndPos) { // 寫入位置在緩衝區內
        buf[(int)(pos - bufStartPos)] = bw;
        bufDirty = true;
        if (pos == fileEndPos + 1) { fileEndPos++; bufUsedSize++; }
    } else { // 寫入位置不在緩衝區
        seek(pos);
        if (pos >= 0 && pos <= fileEndPos && fileEndPos != 0) {
            buf[(int)(pos - bufStartPos)] = bw;
        } else if ((pos == 0 && fileEndPos == 0) || (pos == fileEndPos + 1)) {
            buf[0] = bw; fileEndPos++; bufUsedSize = 1;
        } else { throw new IndexOutOfBoundsException(); }
        bufDirty = true;
    }
    curPos = pos;
    return true;
}

綜合讀寫測試

讀取方式寫入方式耗時(秒)
RandomAccessFileRandomAccessFile95.848
BufferedInputStream + DataInputStreamBufferedOutputStream + DataOutputStream2.935
BufferedRandomAccessFileBufferedOutputStream + DataOutputStream2.813
BufferedRandomAccessFileBufferedRandomAccessFile2.453

綜合讀寫速度已超越 BufferedInput/OutputStream 組合。

優化 BufferedRandomAccessFile

優化原則

  • 調用頻繁的語句最需要優化,效果最顯著。
  • 多重嵌套邏輯判斷時,最可能出現的情況放在最外層。
  • 減少不必要的 NEW 對象。

經典優化案例:seek() 中的位置對齊計算

// 原始寫法:乘除運算較慢
this.bufStartPos = pos * bufBitLen / bufBitLen;

// 優化一:位移運算
this.bufStartPos = (pos << bufBitLen) >> bufBitLen;

// 優化二:位掩碼(最佳,只需一次邏輯與運算)
this.bufStartPos = pos & bufMask; // bufMask = ~((long)bufSize - 1)

seek() 被各函數頻繁調用,優化後效能提升明顯。

優化後測試

讀取方式寫入方式耗時(秒)
BufferedRandomAccessFileBufferedRandomAccessFile2.453
BufferedRandomAccessFile優化版BufferedRandomAccessFile優化版2.197

優化雖不顯著,但確實更快,在老式機器上效果更明顯。

完善功能:多字節讀寫、追加、長度獲取等

補充 append()、write(byte[])、read(byte[])、length()、getFilePointer()、setLength()、close() 等方法,使類功能完整。

多字節讀寫測試(一次讀寫 1024 字節)

讀取方式寫入方式耗時(秒)
BufferedRandomAccessFile完善版BufferedRandomAccessFile完善版0.401

效能大幅躍升,比原始 RandomAccessFile 快 200 多倍!

對比 JDK 1.4 NIO:MappedByteBuffer

JDK 1.4 引入 NIO,MappedByteBuffer 可映射文件實現隨機訪問。測試逐字節複製 12MB 文件:

方案耗時(秒)
MappedByteBuffer + RandomAccessFile1.209
BufferedRandomAccessFile完善版0.401

在 JDK 1.3 環境下,BufferedRandomAccessFile 甚至優於 JDK 1.4 的內存映射方案。若使用 JDK 1.4+,建議優先考慮 MappedByteBuffer;但對於仍在使用 JDK 1.3 的大量存量項目,只需將 RandomAccessFile 替換為 BufferedRandomAccessFile 即可獲得巨大效能提升,無需重寫代碼。

未來展望

讀者可在此基礎上構建多頁緩存及緩存淘汰機制,以應對高強度隨機訪問場景。

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