目录
NIO概述
Java NIO(New I/O,也有称为Non-Blocking IO)在jdk1.4提出,目的在于提高IO的执行效率。NIO基于Channel通道以及Buffer缓冲区来实现I/O操作,其 I/O速度的提高在于把数据的填充和提取放回了操作系统中去执行。例如在Java IO详细总结(一篇涵盖所有)一文中提到为了提供速度用Buffer来装饰原有的I/O,但是不管是在读取数据源数据并将数据放到缓冲区,还是把缓冲区中的数据写入到数据源。这两种操作都是在程序中执行的,而NIO则把上述的操作放回了操作系统因此速度有所提高。此外,本文只讨论文件I/O。
下面是Java NIO类的思维导图:

I/O与NIO区别
- I/O面向流操作每次执行单个字节,NIO面向缓冲Buffer每次操作的的数据由Buffer大小来决定;
- I/O中缓冲区数据的读取或者写入在程序中实现,NIO则是允许在操作系统中完成;
- NIO非阻塞,IO是阻塞的。
FileChannel和ByteBuffer的使用
首先通过一个简单的例子:NIOTest,来了解FileChannel以及ByteBuffer是如何使用的:
// NIOTest.java
try (FileChannel out = new FileOutputStream(new File("D://test.txt")).getChannel();
FileChannel in = new FileInputStream(new File("D://test.txt")).getChannel()) {
//ByteBuffer bb = ByteBuffer.allocateDirect(3);
ByteBuffer bb = ByteBuffer.allocate(10);
// out.write(ByteBuffer.wrap("abc测试".getBytes()));
bb.put("abc测试".getBytes());
bb.flip();
out.write(bb);
bb = ByteBuffer.allocate(10);
// 到达文件末尾返回-1
for (;in.read(bb) != - 1;) {
bb.flip();
// 指定字符集对缓冲区解码
System.out.println(Charset.forName("UTF-8").decode(bb));
bb.clear();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
输出:abc测试上例主要由下面几步来完成文件的输入输出:
- 打开文件对应的读写通道FileChannel;
- 创建缓冲区ByteBuffer;
- 程序填充数据到ByteBuffer,FileChannel写入;
- FileChannel一直读取数据到ByteBuffer中,直到文件末尾;
- 由于用字节写入,打印到控制台的时候通过Charset指定字符集使其能正常显示。
后文将根据源码来了解ByteBuffer以及FileChannel是如果完成NIOTest中的读写过程的。
ByteBuffer
创建ByteBuffer对象
ByteBuffer是一个抽象类不能实例化,因此不能通过显示的new来创建对象。创建ByteBuffer有两种方式:一种是直接缓冲区,通过调用allocateDirect来实现;一种是非直接缓冲区通过allocate来实现。直接缓冲区的优势在于针对其上的I/O操作都在操作系统上执行,相对的非直接缓冲区需要把缓冲区的数据复制到一块中间缓冲区再把中间缓冲区的数据写入内存。很明显前者的速度会快于后者。直接缓冲区开辟的内存空间称为直接内存不在JVM管理中,其不受GC的影响所以文档中建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
非直接缓冲区:通过allocate、wrap方法;
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
public static ByteBuffer wrap(byte[] array) {
return wrap(array, 0, array.length);
}
public static ByteBuffer wrap(byte[] array, int offset, int length)
{
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
从源码可以看到当ByteBuffer调用allocate方法时创建了一个新的对象HeapByteBuffer,该对象继承了ByteBuffer,通过该对象调用父类构造器进行初始化返回子类实例并上转成父类。值得一提的是wrap方法一样可以返回HeapByteBuffer。两者的区别在于是否wrap直接把要写入的数据直接赋值给ByteBuffer底层数据,在本例中相较allocate省去了为底层数组赋值这一步骤。
直接缓冲区:allocateDirect方法
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}可以看到allocateDirect跟allocate方式完全不同,前者通过unsafe类直接向操作系统申请了一块内存(在虚拟机上这块内存称为直接内存)并且后续对缓冲区的所有操作都是在内存上直接进行。此外,如果申请的内存过大会抛出内存溢出。
文章后续在介绍ByteBuffer特性的时候会针对HeapByteBuffer和DirectByteBuffer两种情况进行说明。
ByteBuffer如何写入/读取数据
首先看下ByteBuffer中几个重要参数(部分继承自父类Buffer):
final byte[] hb; // 存放数据的底层数组,当HeapByteBuffer时候为空
final int offset; // 偏移量,数据在底层数组hb中的实际位置为下标索引+偏移量(后文视图缓冲区会用到)
boolean isReadOnly; // 只有当HeapByteBuffer有用,可以指定当前缓冲区为只读
// 下面几个参数由父类Buffer提供
private int mark = -1;// 标记位
private int position = 0;// 下一个要读取或写入的元素的索引,不能为负,并且不能大于limit
// limit表示第一个不应该读取或写入的元素的索引。不能为负,并且不能大于capacity。从数组下标的角度来看,limint表示有多少个元素可以写入通道;还可以容纳多少从通道读取的数据。
// 上述两种描述的出发点在于前者为容量角度来看,后者为数组下标。
// 从本例子的角度看,flip后limit变成9,从前者的角度看hb[9]不应该读取或者写入;从后者来看表示有9个元素可以写入通道
private int limit;
private int capacity;// 数组容量
// 当DirectByteBuffer才有用表示直接内存的地址
long address;针对上述的几个重要参数,有对应的几个主要方法如下:
- capacity():返回缓冲区容量;
- clear():该方法不会删掉底层数据hb内的数据,只是把position位置变成0,limit设成capacity并清除mark变成-1。可以通过该方法来复写缓冲区;
- flip():把limit设置成position,position变成0。该方法可以用来准备从缓冲区读取已经写入的数据;
- limit(int newLimit):设置新的limit;
- mark():标记当前位置,mark值设置成position;
- position(int newPosition):设置新的position;
- remaining():返回position与limit间的值,limit-position。在filp之前表示剩余多少容量,之后表示已使用多少容量;
- hasRemaining():判断position与limit间是否还有值。
下面来理解一下上述几个参数在NIOTest中是如何使用的:
首先创建一个ByteBuffer对象:
ByteBuffer bb = ByteBuffer.allocate(10);
// 对应几个参数的值
final byte[] hb = new byte[10];
final int offset = 0;
boolean isReadOnly = false ;
private int mark = -1;
private int position = 0;
private int limit = 10;
private int capacity = 10;
long address = 0L;
接着放到字节数组:
bb.put("abc测试".getBytes());
public ByteBuffer put(byte[] src, int offset, int length) {
checkBounds(offset, length, src.length);
// 判断要写入的数组长度是否大于当前缓冲区的可用容量
if (length > remaining())
throw new BufferOverflowException();
// 从偏移位置开始存放
int end = offset + length;
for (int i = offset; i < end; i++)
this.put(src[i]);
return this;
}

紧接着调用flip:
bb.flip();
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}

最后调用FileChannel的write方法把ByteBufffer数据写入。
通过前面几个步骤相信已经可以理清这几个参数在ByteBuffer内部是如何工作的了,至于为什么在写入之前要调用flip后文在介绍FileChannel的时候会写到。有兴趣的读者可以参照上文去理解在NIOTest中从FileChannel中读取数据到ByteBuffer这一过程中这几个参数又是怎么完成的。
上述过程是基于HeapByteBuffer完成的,那如果是直接缓冲区DirectByteBuffer的话就更简单了,没有那么繁琐的过程:
// 直接在内存上操作
public ByteBuffer put(byte x) {
unsafe.putByte(ix(nextPutIndex()), ((x)));
return this;
}
public native void putByte(long var1, byte var3);
视图缓冲区
前文提到Buffer提供了七种java基本类型缓冲区,通过ByteBuffer可以得到建立在ByteBuffer缓冲区上的其他基本类型的视图缓冲区:
- asCharBuffer:Char类型的视图缓冲区;
- asDoubleBuffer:Double类型的视图缓冲区;
- asFloatBuffer:Float类型的视图缓冲区;
- asShortBuffer:Short类型的视图缓冲区;
- asIntBuffer:Int类型的视图缓冲区;
- asLongBuffer:Long类型的视图缓冲区;
- asReadOnyBuffer:只可读的Byte类型的缓冲区。
在介绍这些视图缓冲区是如何工作之前先来看看jdk中对应的是哪些类。如果看过源码的话在NIO包下有几十个类诸如ByteBufferAsCharBufferB、ByteBufferAsCharBufferL、ByteBufferAsCharBufferRB、ByteBufferAsCharBufferRL。这边做了一个简单的归纳。
- 所有类名末尾带有R的表示该缓冲区为对应类型的只读缓冲区;
- 所有类名带B、L或者RB、RL的。都表示字节存储次序B代表高位BIG_EDIAN,L表示低位LITLE_EDIAN,后续会谈到;
- 除了HeapByteBuffer以及DirectByteBuffer外各个类型都有自己的HeapXXBuffer;
- 所有类名带U、S或者RU、RS的。都表示在直接内存中处理字节存储次序U表示高位,S表示低位。
在NIOTest示例完成了字符的写入以及读取,但是在读取的时候需要显示的通过Charset来进行转换。如果一开始就使用Char视图缓冲区的话就不需要Charset转换了。下面来对NIOTest进行重写:
ByteBuffer bb = ByteBuffer.allocate(10);
//.put("abc测试".getBytes());
bb.asCharBuffer().put("abc测试".toCharArray());
out.write(bb);
bb = ByteBuffer.allocate(10);
// 到达文件末尾返回-1
for (;in.read(bb) != - 1;) {
bb.flip();
System.out.println(bb.asCharBuffer());
// 指定字符集对缓冲区解码
//System.out.println(Charset.forName("UTF-8").decode(bb));
bb.clear();
}
输出:abc测试可以看到相较之前的代码除了不用在通过Charset解码外,在FileChannel写入之前也不需要调用flip方法了。具体原因通过源码来一探究竟:
// 通过ByteBuffer获取Char缓冲区视图
public CharBuffer asCharBuffer() {
int size = this.remaining() >> 1;
int off = offset + position();
return (bigEndian
? (CharBuffer)(new ByteBufferAsCharBufferB(this,-1,0,size,size,off))
: (CharBuffer)(new ByteBufferAsCharBufferL(this,-1,0,size,size,off)));
}
// 默认采用高位字节存储所以创建ByteBufferAsCharBufferB对象
ByteBufferAsCharBufferB(ByteBuffer bb,
int mark, int pos, int lim, int cap,
int off)
{
super(mark, pos, lim, cap);
// 把ByteBuffer bb作为ByteBufferAsCharBufferB的成员对象
this.bb = bb;
offset = off;
}
// 转化后开始执行put操作
public final CharBuffer put(char[] src) {
return put(src, 0, src.length);
}
public CharBuffer put(char[] src, int offset, int length) {
checkBounds(offset, length, src.length);
if (length > remaining())
throw new BufferOverflowException();
int end = offset + length;
for (int i = offset; i < end; i++)
// this指向的是上转成CharBuffer的ByteBufferAsCharBufferB对象
this.put(src[i]);
return this;
}
// 往当前对象ByteBufferAsCharBufferB的成员对象ByteBuffer放入数据
public CharBuffer put(char x) {
Bits.putCharB(bb, ix(nextPutIndex()), x);
return this;
}
final int nextPutIndex() {
if (position >= limit)
throw new BufferOverflowException();
// position的增加对应的是当前操作对象ByteBufferAsCharBufferB的position参数而
// 不是原先ByteBuffer bb,所以原先的bb中的position还是0。
// 因此不能再使用flip,否则此时limit = position = 0,那么写入的数据就是0
return position++;
}
static void putCharB(ByteBuffer bb, int bi, char x) {
bb._put(bi , char1(x));
bb._put(bi + 1, char0(x));
}
// char类型为两个字节,1个字节8位
private static byte char1(char x) { return (byte)(x >> 8); }
private static byte char0(char x) { return (byte)(x ); }
// 放入ByteBuffer数组中
void _put(int i, byte b) { // package-private
hb[i] = b;
}
从上述源码中可以得知,即便是操作转换后的视图缓冲区但是其根本还是在原先的ByteBuffer底层数组。因此如果一旦底层数组的剩余容量不足以放入对应的数据例如把上例allocate(10)改成5就会抛出BufferOverflowException异常。
下图是各个类型对应的字节数:

其他的基本类型视图缓冲区,有兴趣的读者可以参照上例字节缓冲区转成字符缓冲区的逻辑处理顺序进行测试。
ByteOrder
前文提到了多次字节的高低位写入,在NIO中通过类ByteOrder来实现。该类比较简单,返回一个高位的ByteOrder或者低位的ByteOrder或者是通过nativeOrder方法返回一个本机的字节写入顺序。最后通过ByteBuffer的order方法传入该ByteOrder对象以此来修改字节的写入顺序。
FileChannel
创建FileCHannel对象
通过思维导图可以看到NIO包下所有的Channel类都是由原先已有的类调用getChannel方法得到。下面通过NIOTest例子结合源码来一探究竟:
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, false, true, append, this);
}
return channel;
}
}
// public class FileChannelImpl extends FileChannel
public static FileChannel open(FileDescriptor var0, String var1, boolean var2, boolean var3, boolean var4, Object var5) {
return new FileChannelImpl(var0, var1, var2, var3, var4, var5);
}
private FileChannelImpl(FileDescriptor var1, String var2, boolean var3, boolean var4, boolean var5, Object var6) {
this.fd = var1;// 文件描述符
this.readable = var3;// 可读通道
this.writable = var4;// 可写通道
this.append = var5;// 新写入的字符是否追加到文件末尾
this.parent = var6;// 调用FileChannelImple静态方法open的类,本例中是FileOutputStream
this.path = var2;// 文件路径
this.nd = new FileDispatcherImpl(var5);
}
在NIOTest中通过FileOutputStream的getChannel方法返回了一个继承FileChannel类的FileChannelImpl,在FileChannelImpl的初始化中可以看到传入了多个参数,其中readable以及writable两个参数与你是通过FileOutputStream还是FileInputStream创建FileChannel有关。
FileChannel如何写入数据
接着来看看FileChannel是如何实现写入的:
public int write(ByteBuffer var1) throws IOException {
this.ensureOpen();// 确保当前通道没有被关闭-close
if (!this.writable) {// 当前必须是写通道
throw new NonWritableChannelException();
} else {
Object var2 = this.positionLock;// 对象锁
synchronized(this.positionLock) {
int var3 = 0;
int var4 = -1;
try {
// 可中断异常开始
this.begin();
var4 = this.threads.add();
if (!this.isOpen()) {
byte var12 = 0;
return var12;
} else {
do {
// 写入数据
var3 = IOUtil.write(this.fd, var1, -1L, this.nd);
} while(var3 == -3 && this.isOpen());
// 写入字节数
int var5 = IOStatus.normalize(var3);
return var5;
}
} finally {
this.threads.remove(var4);
// 可中断异常结束
this.end(var3 > 0);
assert IOStatus.check(var3);
}
}
}
}
从Java IO详细总结(一篇涵盖所有)一文中可以知道文件IO是阻塞的,通过在NIO中新加了抽象类AbstractInterruptibleChannel来实现可中断/打断的通道,其实现的方式就是通过try-finally块首先调用begin方法,最后在finally中调用end方法来确保通道是否被中断。下面来看看begin以及end的源码:
// public abstract class AbstractInterruptibleChannel
private Interruptible interruptor;// 可中断接口,定义了interrupt方法
private volatile Thread interrupted;
protected final void begin() {
if (interruptor == null) {
// 创建与当前线程有关的Interruptible子类并重写了interrupt方法
interruptor = new Interruptible() {
public void interrupt(Thread target) {
synchronized (closeLock) {
if (!open)
return;
open = false;
interrupted = target;
try {
AbstractInterruptibleChannel.this.implCloseChannel();
} catch (IOException x) { }
}
}};
}
// 把interruptor绑定到当前线程中去
blockedOn(interruptor);
Thread me = Thread.currentThread();
// 判断当前线程中断状态
if (me.isInterrupted())
interruptor.interrupt(me);
}
protected void implCloseChannel() throws IOException {
// 该处删去了与文件锁相关部分的代码
this.threads.signalAndWait();
// 关闭连接
if (this.parent != null) {
((Closeable)this.parent).close();
} else {
this.nd.close(this.fd);
}
}
protected final void end(boolean completed)
throws AsynchronousCloseException
{
// 释放掉原先的interruptor
blockedOn(null);
Thread interrupted = this.interrupted;
// 如果被打断了抛出异常
if (interrupted != null && interrupted == Thread.currentThread()) {
interrupted = null;
throw new ClosedByInterruptException();
}
if (!completed && !open)
throw new AsynchronousCloseException();
}
// Thread 类中的方法
void blockedOn(Interruptible b) {
synchronized (blockerLock) {
blocker = b;
}
}
// Thread 类中的方法
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
从源码可以看到AbstractInterruptibleChannel的begin方法通过创建新的Interruptible对象绑定到当前线程,当检测到当前通道线程被中断后就会调用改对象的interrupt方法,最后通过end方法来抛出中断异常。
下面一个小案例实践下AbstractInterruptibleChannel的功能,同时比较IO与NIO各自在多线程环境下打断的情况:
// 该示例通过标准输入流来实现阻塞的情况
Thread t = new Thread(()->{
System.out.println("Thread start");
InputStream is = System.in;
try {
FileOutputStream fos = new FileOutputStream(new File("D://test.txt"));
// fos.write(is.read());
// if (Thread.currentThread().isInterrupted()) {
// System.out.println("io has been interrupted");
// }
FileChannel fc = fos.getChannel();
ByteBuffer bb = ByteBuffer.allocate(1024);
bb.asIntBuffer().put(is.read());
fc.write(bb);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Thread end");
});
t.start();
t.interrupt();
// IO的情况下(使用注释的代码)键盘输入后不会抛出异常且文件还是正常写入;
// NIO的情况下键盘输入后程序抛出ClosedByInterruptException异常。接着看看核心方法IOUtil.write内部是如何实现的:
// fd:前文介绍过的文件描述符
// var1:ByteBuffer对象
// -1L:指定当前写入文件的位置,默认不指定为-1。可通过带有位置的write方法指定该参数
// nd:前文介绍过的FileDispatcher
var3 = IOUtil.write(this.fd, var1, -1L, this.nd);
static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
// 判断ByteBuffer是否来自直接内存
if (var1 instanceof DirectBuffer) {
return writeFromNativeBuffer(var0, var1, var2, var4);
} else {
int var5 = var1.position();
int var6 = var1.limit();
assert var5 <= var6;
// 要写入字节个数
int var7 = var5 <= var6 ? var6 - var5 : 0;
// 创建一个临时的直接缓冲区
ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7);
int var10;
try {
// 把传进来的ByteBuffer底层数组的值复制到新的直接内存缓冲区
var8.put(var1);
var8.flip();
var1.position(var5);
// 写入数据
int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
if (var9 > 0) {
var1.position(var5 + var9);
}
var10 = var9;
} finally {
// 把临时的直接缓冲区放入当前线程内存空间ThreadLocal以便复用
Util.offerFirstTemporaryDirectBuffer(var8);
}
return var10;
}
}
public static ByteBuffer getTemporaryDirectBuffer(int var0) {
// 如果写入空间比较大直接创建直接缓冲区
if (isBufferTooLarge(var0)) {
return ByteBuffer.allocateDirect(var0);
} else {
// 从当前线程本地存储知直接获取
Util.BufferCache var1 = (Util.BufferCache)bufferCache.get();
ByteBuffer var2 = var1.get(var0);
if (var2 != null) {
return var2;
} else {
if (!var1.isEmpty()) {
var2 = var1.removeFirst();
free(var2);
}
// 如果本地存储没有数据重新创建一个直接缓冲区
return ByteBuffer.allocateDirect(var0);
}
}
}
private static int writeFromNativeBuffer(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
int var5 = var1.position();
int var6 = var1.limit();
assert var5 <= var6;
int var7 = var5 <= var6 ? var6 - var5 : 0;
boolean var8 = false;
if (var7 == 0) {
return 0;
} else {
int var9;
// 区分不同的写入位置
if (var2 != -1L) {
var9 = var4.pwrite(var0, ((DirectBuffer)var1).address() + (long)var5, var7, var2);
} else {
var9 = var4.write(var0, ((DirectBuffer)var1).address() + (long)var5, var7);
}
// 更新position
if (var9 > 0) {
var1.position(var5 + var9);
}
return var9;
}
}
static void offerFirstTemporaryDirectBuffer(ByteBuffer var0) {
if (isBufferTooLarge(var0)) {
// 直接缓冲区过大直接释放
free(var0);
} else {
assert var0 != null;
// ThreadLocal包装的内部类BufferCache
Util.BufferCache var1 = (Util.BufferCache)bufferCache.get();
// 如果当前本地线程容量没有达到上限,则把临时缓冲区放到第一个
if (!var1.offerFirst(var0)) {
// 到达上限后释放
free(var0);
}
}
}
从上述源码中可以得到几个结论:
- 直接缓冲区的效率比非直接缓冲区的效率要高,因为少了一次数据在直接缓冲区跟非直接缓冲区复制的过程;
- 程序启动时IOUtil内部通过ThreadLocal事先创建一块固定大小的缓冲区,通道会通过它获取一块临时缓冲区因此下次写入相同数据的时候速度更快,也避免了又开辟一块直接缓冲区。
通道的读取跟写入差不多,这边不再赘述。
FileChannel文件加锁
通过FileChannel可以对文件进行加锁,加锁的类型有共享锁以及独占锁两种。需要注意的事加锁的动作在操作系统上完成的,因此如果操作系统不支持共享锁那么就只能使用独占锁。
有两种类型的方法可以对文件进行加锁,第一种是lock方法:

lock方法会阻塞直到获取到该文件的锁,除非通道关闭或者被中断,第二种带参数的lock方法可以指定加锁的文件区域,并且可以指定是否是共享锁。
第二种tryLock:

跟前者的区别就是tryLock为非阻塞的,如果拿不到锁的话直接返回Null。
通过两种方法返回的FileLock对象调用release方法就可以释放锁。
文件映射
通过map方法可以将文件中的某个区域直接映射到内存中;对于较大的文件,这通常比调用普通的 read 或 write 方法更为高效。

MapMode有三种模式分别为只读、只写、读写。