在 JDK 1.3 時代,RandomAccessFile 是實現文件隨機讀寫的核心類。然而,其 I/O 效能與其他語言的同類功能相比差距巨大,嚴重拖慢程序運行速度。本文將深入剖析其源碼,找出關鍵瓶頸,並逐步構建一個「性價比」俱佳的緩衝隨機訪問類 BufferedRandomAccessFile。
先做一個基礎測試:逐字節複製一個 12MB 文件(涉及讀與寫)。
| 讀取方式 | 寫入方式 | 耗時(秒) |
|---|---|---|
| RandomAccessFile | RandomAccessFile | 95.848 |
| BufferedInputStream + DataInputStream | BufferedOutputStream + DataOutputStream | 2.935 |
兩者相差約 32 倍,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 操作。
以 BufferedInputStream 為例,內部維護一個 byte buf[]。讀取時,若數據在緩衝區中,直接從記憶體讀取;否則從磁盤填充緩衝區再讀取。絕大多數操作是對記憶體 buf[] 的操作。
public synchronized int read() throws IOException {
ensureOpen();
if (pos >= count) { fill(); ... }
return buf[pos++] & 0xff; // 直接從記憶體讀取
}
BufferedOutputStream 同理,寫入先進緩衝區,滿了才 flushBuffer() 寫入磁盤。
即便對記憶體操作上萬次,耗時也遠小於磁盤一次 I/O。緩衝機制正是通過增加記憶體存取,減少磁盤 I/O 開銷,提升 32 倍效能。
隨機訪問類實現 DataInput/DataOutput 接口,無法直接繼承 FilterInputStream。需自行實現緩衝邏輯。
核心邏輯:
關鍵代碼片段(簡化版):
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);
}
}
| 讀取方式 | 寫入方式 | 耗時(秒) |
|---|---|---|
| RandomAccessFile | RandomAccessFile | 95.848 |
| BufferedRandomAccessFile | BufferedOutputStream + DataOutputStream | 2.813 |
| BufferedInputStream + DataInputStream | BufferedOutputStream + DataOutputStream | 2.935 |
讀取速度明顯提升,與 BufferedInputStream 平起平坐。
核心邏輯:
關鍵代碼(簡化版):
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;
}
| 讀取方式 | 寫入方式 | 耗時(秒) |
|---|---|---|
| RandomAccessFile | RandomAccessFile | 95.848 |
| BufferedInputStream + DataInputStream | BufferedOutputStream + DataOutputStream | 2.935 |
| BufferedRandomAccessFile | BufferedOutputStream + DataOutputStream | 2.813 |
| BufferedRandomAccessFile | BufferedRandomAccessFile | 2.453 |
綜合讀寫速度已超越 BufferedInput/OutputStream 組合。
// 原始寫法:乘除運算較慢
this.bufStartPos = pos * bufBitLen / bufBitLen;
// 優化一:位移運算
this.bufStartPos = (pos << bufBitLen) >> bufBitLen;
// 優化二:位掩碼(最佳,只需一次邏輯與運算)
this.bufStartPos = pos & bufMask; // bufMask = ~((long)bufSize - 1)
seek() 被各函數頻繁調用,優化後效能提升明顯。
| 讀取方式 | 寫入方式 | 耗時(秒) |
|---|---|---|
| BufferedRandomAccessFile | BufferedRandomAccessFile | 2.453 |
| BufferedRandomAccessFile優化版 | BufferedRandomAccessFile優化版 | 2.197 |
優化雖不顯著,但確實更快,在老式機器上效果更明顯。
補充 append()、write(byte[])、read(byte[])、length()、getFilePointer()、setLength()、close() 等方法,使類功能完整。
| 讀取方式 | 寫入方式 | 耗時(秒) |
|---|---|---|
| BufferedRandomAccessFile完善版 | BufferedRandomAccessFile完善版 | 0.401 |
效能大幅躍升,比原始 RandomAccessFile 快 200 多倍!
JDK 1.4 引入 NIO,MappedByteBuffer 可映射文件實現隨機訪問。測試逐字節複製 12MB 文件:
| 方案 | 耗時(秒) |
|---|---|
| MappedByteBuffer + RandomAccessFile | 1.209 |
| BufferedRandomAccessFile完善版 | 0.401 |
在 JDK 1.3 環境下,BufferedRandomAccessFile 甚至優於 JDK 1.4 的內存映射方案。若使用 JDK 1.4+,建議優先考慮 MappedByteBuffer;但對於仍在使用 JDK 1.3 的大量存量項目,只需將 RandomAccessFile 替換為 BufferedRandomAccessFile 即可獲得巨大效能提升,無需重寫代碼。
讀者可在此基礎上構建多頁緩存及緩存淘汰機制,以應對高強度隨機訪問場景。