Java NIO 教程 MappedByteBuffer


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_WRITEMapMode.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 就告訴你這么多了,有什么問題盡管提、有什么想法隨時找我交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM