Java NIO 教程 MappedByteBuffer
之前跟大家說過,要講 MappedByteBuffer, 現在我來履行承諾了。
首先從大體上講一下 MappedByteBuffer 究竟是什么。從繼承結構上來講,MappedByteBuffer 繼承自 ByteBuffer,所以 ByteBuffer 有的能力它全有;像變動 position 和 limit 指針啦、包裝一個其他種類 Buffer 的視圖啦,都可以。“MappedByteBuffer” 為何而來?吾輩心中亦有惑(熊貓人之謎的梗)用一個字來概括就是快
為什么快?因為它使用 direct buffer 的方式讀寫文件內容,這種方式的學名叫做內存映射。這種方式直接調用系統底層的緩存,沒有 JVM 和系統之間的復制操作,所以效率大大的提高了。而且由於它這么快,還可以用它來在進程(或線程)間傳遞消息,基本上能達到和 “共享內存頁” 相同的作用,只不過它是依托實體文件來運行的。
而且它還有另一種能力。就是它可以讓我們讀寫那些因為太大而不能放進內存中的文件。有了它,我們就可以假定整個文件都放在內存中(實際上,大文件放在內存和虛擬內存中),基本上都可以將它當作一個特別大的數組來訪問,這樣極大的簡化了對於大文件的修改等操作。
下面我們開始介紹它的用法了
FileChannel 提供了 map 方法來把文件映射為 MappedByteBuffer: MappedByteBuffer map(int mode,long position,long size); 可以把文件的從 position 開始的 size 大小的區域映射為 MappedByteBuffer,mode 指出了可訪問該內存映像文件的方式,共有三種,分別為:
MapMode.READ_ONLY
(只讀): 試圖修改得到的緩沖區將導致拋出 ReadOnlyBufferException。
MapMode.READ_WRITE
(讀 / 寫): 對得到的緩沖區的更改最終將寫入文件;但該更改對映射到同一文件的其他程序不一定是可見的(無處不在的 “一致性問題” 又出現了)。
MapMode.PRIVATE
(專用): 可讀可寫, 但是修改的內容不會寫入文件, 只是 buffer 自身的改變,這種能力稱之為”copy on write”
再簡單的說一下,MappedByteBuffer 較之 ByteBuffer 新增的三個方法
- fore() 緩沖區是 READ_WRITE 模式下,此方法對緩沖區內容的修改強行寫入文件
- load() 將緩沖區的內容載入內存,並返回該緩沖區的引用
- isLoaded() 如果緩沖區的內容在物理內存中,則返回真,否則返回假
下面代碼終於出場了
int length = 0x8FFFFFF;//一個byte占1B,所以共向文件中存128M的數據
try (FileChannel channel = FileChannel.open(Paths.get("src/c.txt"),
StandardOpenOption.READ, StandardOpenOption.WRITE);) {
MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length);
for(int i=0;i<length;i++) {
mapBuffer.put((byte)0);
}
for(int i = length/2;i<length/2+4;i++) {
//像數組一樣訪問
System.out.println(mapBuffer.get(i));
}
}
上面是 MappedByteBuffer 最基本的應用,而下面這段代碼主要是測試它到底有多快,
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class TestMappedByteBuffer {
private static int length = 0x2FFFFFFF;//1G
private abstract static class Tester {
private String name;
public Tester(String name) {
this.name = name;
}
public void runTest() {
System.out.print(name + ": ");
long start = System.currentTimeMillis();
test();
System.out.println(System.currentTimeMillis()-start+" ms");
}
public abstract void test();
}
private static Tester[] testers = {
new Tester("Stream RW") {
public void test() {
try (FileInputStream fis = new FileInputStream(
"src/a.txt");
DataInputStream dis = new DataInputStream(fis);
FileOutputStream fos = new FileOutputStream(
"src/a.txt");
DataOutputStream dos = new DataOutputStream(fos);) {
byte b = (byte)0;
for(int i=0;i<length;i++) {
dos.writeByte(b);
dos.flush();
}
while (dis.read()!= -1) {
}
} catch (IOException e) {
e.printStackTrace();
}
}
},
new Tester("Mapped RW") {
public void test() {
try (FileChannel channel = FileChannel.open(Paths.get("src/b.txt"),
StandardOpenOption.READ, StandardOpenOption.WRITE);) {
MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length);
for(int i=0;i<length;i++) {
mapBuffer.put((byte)0);
}
mapBuffer.flip();
while(mapBuffer.hasRemaining()) {
mapBuffer.get();
}
} catch (IOException e) {
e.printStackTrace();
}
}
},
new Tester("Mapped PRIVATE") {
public void test() {
try (FileChannel channel = FileChannel.open(Paths.get("src/c.txt"),
StandardOpenOption.READ, StandardOpenOption.WRITE);) {
MappedByteBuffer mapBuffer = channel.map(FileChannel.MapMode.PRIVATE, 0, length);
for(int i=0;i<length;i++) {
mapBuffer.put((byte)0);
}
mapBuffer.flip();
while(mapBuffer.hasRemaining()) {
mapBuffer.get();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
public static void main(String[] args) {
for(Tester tester:testers) {
tester.runTest();
}
}
}
先從整體上提一句上面的代碼,runTest() 是一個模板方法,並且引用了一個未實現的 test() 方法;通過匿名內部類的實現,填充了測試內容。
再來說上面代碼的測試結果。用傳統流的方式,當然是最慢的,但應該是由於用的數據量是 1G,無法全部讀入內存,所以它根本無法完成測試。剩下兩種MapMode.READ_WRITE
和MapMode.PRIVATE
各有特點,首先說MapMode.READ_WRITE
,它的速度每次差別較大,在 0.6s 和 8s 之間波動,而且很不穩定。但MapMode.PRIVATE
就穩得出奇,一直是 1.1s 到 1.2s 之間。但無論是哪個速度都是十分驚人的。但是 MappedByteBuffer 也有不足,就是在數據量很小的時候,表現比較糟糕,那是因為 direct buffer 的初始化時間較長,所以建議大家只有在數據量較大的時候,在用 MappedByteBuffer。
還要強調的一點是,MappedByteBuffer 存在內存占用和文件關閉等不確定問題。被 MappedByteBuffer 打開的文件只有在垃圾收集時才會被關閉,而這個點是不確定的。javadoc 里是這么說的:
A mapped byte buffer and the file mapping that it represents remain valid until the buffer itself is garbage-collected. ——JavaDoc
關於 MappedByteBuffer 就告訴你這么多了,有什么問題盡管提、有什么想法隨時找我交流。