一、RandomAccessFile
簡單示例:
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* @author debo
* @date 2020-06-27
*/
public class RandomAccessFileTest {
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("/home/debo/tmp.txt", "rw");
String msg = "你好,world!";
System.out.println("初始文件指針位置:" + raf.getFilePointer());
raf.write(msg.getBytes());
System.out.println("第一次寫完之後文件指針位置:" + raf.getFilePointer());
// 手動修改讀寫指針位置
raf.seek(9);
System.out.println("修改後的文件指針位置:" + raf.getFilePointer());
byte b = raf.readByte();
System.out.println("第一次讀完之後文件指針位置:" + raf.getFilePointer() + ",讀到的內容:" + (char) b);
// 修改指針位置10處的字節內容
raf.writeBytes("K");
System.out.println("第二次寫完之後文件指針位置:" + raf.getFilePointer());
byte[] dst = new byte[(int) raf.length()];
// 指針歸零
raf.seek(0);
// 將數據全部讀取出來
raf.read(dst);
System.out.println("第二次讀完之後文件指針位置:" + raf.getFilePointer() + ",讀到的內容:" + new String(dst));
raf.close();
}
}
輸出如下:
初始文件指針位置:0
第一次寫完之後文件指針位置:15
修改後的文件指針位置:9
第一次讀完之後文件指針位置:10,讀到的內容:w
第二次寫完之後文件指針位置:11
第二次讀完之後文件指針位置:15,讀到的內容:你好,wKrld!
對比流式IO,隨機訪問IO的優勢還是很明顯的,通過移動讀寫指針,可以讀任意位置和寫任意位置,非常靈活。還有一個相比流式IO重要的區別是,用 FileOutputStream 去寫文件時,如果文件已存在,則會刪掉原有的文件或者在原有的文件末尾追加數據,而如果使用 RandomAccessFile 去寫文件,如果文件已存在,依然可以通過移動讀寫指針來操縱已存在文件的任意位置。可以想象一下,如果已存在文件大小是150B,現在要將新內容寫入指針位置15~30區間上,這個過程就好像是在編輯文件,只修改指定位置處的數據,而其它位置的數據保持不變。
我們知道,在寫文件時,對文件的修改不會被立即寫入到磁盤,而是先修改文件在內核的 page cache,操作系統會在合適的時機自動將修改同步到磁盤,這樣就有可能會出現斷電以後數據丟失的情況,爲了進行更加嚴謹的數據控制,可以在每次 RandomAccessFile.write 之後調用 RandomAccessFile.getFD().sync() 強制將修改同步寫入到磁盤。
RandomAccessFile 的訪問模式有以下幾種:
- r:表示以只讀方式打開文件
- rw:表示以讀寫方式打開文件
- rws:表示以讀寫方式打開文件,同時每次對文件的修改都會被同步寫入磁盤
- rwd:表示以讀寫方式打開文件,同時每次對文件內容的修改都會被同步寫入磁盤,和rws的區別在於,rws模式下,會將文件內容和文件元數據的修改都同步到磁盤。
rws 和 rwd 模式下,每一次對文件的寫操作都會等待同步到磁盤後才返回,不需要再額外調用 RandomAccessFile.getFD().sync() 方法,但是很顯然沒有 rw 模式下再手動調用同步刷盤來得靈活。
二、FileChannel
FileChannel 的功能和 RandomAccessFile 的功能大體一致,都通過文件位置指針來控制對文件的隨機讀寫,FileChannel 實例可以通過 RandomAccessFile.getChannel()、FileInputStream.getChannel()、FileOutputStream.getChannel() 獲取,JDK1.7之後還可以通過 FileChannel.open() 靜態方法獲取。簡單示例如下:
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* @author debo
* @date 2020-06-27
*/
public class FileChannelTest {
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("/home/debo/tmp.txt", "rw");
FileChannel fileChannel = raf.getChannel();
String msg = "你好,world!";
System.out.println("初始文件指針位置:" + fileChannel.position());
fileChannel.write(ByteBuffer.wrap(msg.getBytes()));
System.out.println("第一次寫完之後文件指針位置:" + fileChannel.position());
// 手動修改讀寫指針位置
fileChannel.position(9);
System.out.println("修改後的文件指針位置:" + fileChannel.position());
ByteBuffer dst = ByteBuffer.allocate(1);
fileChannel.read(dst);
dst.flip();
System.out.println("第一次讀完之後文件指針位置:" + fileChannel.position() + ",讀到的內容:" + (char) dst.get());
// 修改指針位置10處的字節內容
fileChannel.write(ByteBuffer.wrap("K".getBytes()));
System.out.println("第二次寫完之後文件指針位置:" + fileChannel.position());
dst = ByteBuffer.allocate((int) fileChannel.size());
// 指針歸零
fileChannel.position(0);
// 將數據全部讀取出來
fileChannel.read(dst);
dst.flip();
System.out.println("第二次讀完之後文件指針位置:" + fileChannel.position() + ",讀到的內容:" + new String(dst.array()));
fileChannel.close();
}
}
輸出如下:
初始文件指針位置:0
第一次寫完之後文件指針位置:15
修改後的文件指針位置:9
第一次讀完之後文件指針位置:10,讀到的內容:w
第二次寫完之後文件指針位置:11
第二次讀完之後文件指針位置:15,讀到的內容:你好,wKrld!
同樣,FileChannel 也提供了手動刷盤的方法 FileChannel.force 將 page cache 中的文件修改同步寫入到磁盤。
需要注意的是,通過 FileInputStream 獲取的 FileChannel 不支持寫操作,通過 FileOutputStream 獲取的 FileChannel 不支持讀操作,通過 “r” 模式的 RandomAccessFile 獲取的 FileChannel 不支持寫操作等等,這些都是在轉換 FileChannel 時的應有之義。
三、MappedByteBuffer
mmap系統調用可以將文件的一個指定區域直接映射到用戶進程的虛擬地址空間,這樣當用戶進程操作文件時,就像操作分配給自己的內存一樣。更詳細地說,就是以普通方式去讀寫文件時,會產生 read/write 系統調用,而通過 mmap 方式操作文件時,在文件讀寫的過程中不會產生 read/write 系統調用。
java 中使用 MappedByteBuffer 來表示這塊內存映射區域,這個類的實例通過 FileChannel.map() 方法獲取,示例如下:
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
/**
* @author debo
* @date 2020-06-27
*/
public class MappedByteBufferWriteTest {
private static final long COUNT = 1000_0000L;
public static void main(String[] args) throws IOException {
RandomAccessFile raf = new RandomAccessFile("/home/debo/tmp.txt", "rw");
FileChannel fileChannel = raf.getChannel();
String msg = "你好,world!";
// 內存映射區域總大小
long size = msg.getBytes().length * COUNT;
long start = System.currentTimeMillis();
MappedByteBuffer map = fileChannel.map(MapMode.READ_WRITE, 0, size);
for (int i = 0; i < COUNT; i++) {
map.put(msg.getBytes());
}
System.out.println("當前文件指針位置:" + map.position());
raf.close();
System.out.println(String.format("耗時:%d毫秒", System.currentTimeMillis() - start));
}
}
mmap 同樣支持通過文件指針隨機讀寫文件,類似地,mmap也提供了手動刷盤的方法 MappedByteBuffer.force()。