JDK 1.3 時代,RandomAccessFile 單字節讀寫直接調用 Native 方法,磁盤 I/O 開銷巨大。本文從源碼分析入手,逐步構建 BufferedRandomAccessFile,通過內存緩衝將 12MB 文件複製耗時從 95 秒降至 0.4 秒,性能超越 JDK 1.4 的 MappedByteBuffer。
| 讀取方式 | 寫入方式 | 耗時(秒) |
|---|---|---|
| RandomAccessFile | RandomAccessFile | 95.848 |
| BufferedInputStream | BufferedOutputStream | 2.935 |
根因:RandomAccessFile.read()/write() 每字節一次 Native 調用 → 磁盤 I/O。而 Buffered 流通過內存 buf[] 批量讀寫,磁盤 I/O 減少數量級。
public final byte readByte() throws IOException {
int ch = this.read(); // Native 方法,每字節一次系統調用
if (ch < 0) throw new EOFException();
return (byte)ch;
}
public native int read() throws IOException;
public native void write(int b) throws IOException;
private byte[] buf = new byte[2048]; // 內存緩衝區
public synchronized int read() throws IOException {
if (pos >= count) fill(); // 緩衝區空則批量填充
return buf[pos++] & 0xff; // 直接內存讀取,納秒級
}
private void fill() { ... in.read(buf, pos, buf.length - pos); } // 批量磁盤讀取
private byte[] buf = new byte[512];
public synchronized void write(int b) throws IOException {
if (count >= buf.length) flushBuffer(); // 緩衝區滿則批量刷盤
buf[count++] = (byte)b; // 直接內存寫入
}
byte[] buf 作為讀寫共用緩衝區(默認 1024 字節)bufstartpos/bufendpos:緩衝區映射的文件偏移範圍curpos:當前邏輯文件指針bufdirty:寫緩衝區是否需刷盤fileendpos:文件實際末尾位置(支持追加)public byte read(long pos) throws IOException {
// 1. 目標位置是否在緩衝區內
if (pos < bufstartpos || pos > bufendpos) {
flushbuf(); // 先刷寫緩衝
seek(pos); // 重新定位並填充緩衝區
}
curpos = pos;
return buf[(int)(pos - bufstartpos)]; // 直接內存讀取
}
private 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 || pos == fileendpos + 1) { // 追加位置
bufstartpos = pos;
bufusedsize = 0;
}
bufendpos = bufstartpos + bufsize - 1;
}
curpos = pos;
}
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 <= fileendpos) { // 修改現有數據
buf[(int)(pos - bufstartpos)] = bw;
} else if (pos == fileendpos + 1) { // 追加
buf[0] = bw;
fileendpos++; bufusedsize = 1;
} else throw new IndexOutOfBoundsException();
bufdirty = true;
}
curpos = pos;
return true;
}
| 階段 | 優化點 | 耗時(秒) |
|---|---|---|
| 原始 RandomAccessFile | 無緩衝 | 95.848 |
| + 讀緩衝 | BufferedRandomAccessFile 讀 | 2.813 |
| + 寫緩衝 | 雙向緩衝 | 2.453 |
| + 位運算優化 seek | pos & bufmask 替代乘除 | 2.197 |
| + 批量讀寫 API | read(byte[])/write(byte[]) | 0.401 |
// 原始:乘除運算較慢
bufstartpos = pos * bufbitlen / bufbitlen;
// 優化 1:移位運算
bufstartpos = (pos << bufbitlen) >> bufbitlen;
// 優化 2:位與運算(最優,bufmask = ~(bufsize-1) 預計算)
bufstartpos = pos & bufmask; // 一次邏輯運算,極快
public void write(byte[] b, int off, int len) throws IOException {
long writeEndPos = curpos + len - 1;
if (writeEndPos <= bufendpos) { // 全在緩衝區
System.arraycopy(b, off, buf, (int)(curpos - bufstartpos), len);
bufdirty = true;
bufusedsize = (int)(writeEndPos - bufstartpos + 1);
} else { // 跨緩衝區或超出,直接走底層
super.seek(curpos);
super.write(b, off, len);
}
if (writeEndPos > fileendpos) fileendpos = writeEndPos;
seek(writeEndPos + 1);
}
public int read(byte[] b, int off, int len) throws IOException {
long readEndPos = curpos + len - 1;
if (readEndPos <= bufendpos && readEndPos <= fileendpos) { // 全在緩衝區
System.arraycopy(buf, (int)(curpos - bufstartpos), b, off, len);
} else { // 超出緩衝區
if (readEndPos > fileendpos) len = (int)(length() - curpos + 1);
super.seek(curpos);
len = super.read(b, off, len);
readEndPos = curpos + len - 1;
}
seek(readEndPos + 1);
return len;
}
| 方案 | 耗時(秒) |
|---|---|
| MappedByteBuffer + RandomAccessFile | 1.209 |
| BufferedRandomAccessFile (完善版) | 0.401 |
純 Java 實現反超 NIO 內存映射,得益於順序訪問友好、無頁錯誤開銷、JIT 優化友好。
BufferedRandomAccessFile 類new RandomAccessFile(...) 替換為 new BufferedRandomAccessFile(...)注意:JDK 1.4+ 新項目首選MappedByteBuffer或FileChannel。本方案針對存量 JDK 1.3 項目無痛升級。
對於高強度隨機訪問,可構建:
Map 緩存多個文件區塊)