6 NIO(内存映射文件)

Wu Jun 2020-02-29 16:31:31
05 Java > 00 Java 基础 > 09 IO

对比

1 核心概念

1.1 通道

通道 Channel 是对原 I/O 包中的流的模拟,通道是双向的。读写数据通过 Channel,再到通道。可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性。

获取通道
FileChannel channel = FileChannel.open(path,options); 

1.2 缓冲区

缓冲区 Buffer 是一个容器对象,包含一些要写入或者刚读出的数据。通道读写的数据首先放到缓冲区。

缓冲区实质上是一个数组。通常是字节数组,也可使用其他数组。缓冲区不仅仅是一个数组,还提供了对数据的结构化访问,且还可以跟踪系统的读/写进程。

获取缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte array[] = new byte[1024];  
ByteBuffer buffer = ByteBuffer.wrap( array ); 
ByteBuffer buffer = channel.map(FileChannel.MapMode.PRIVATE, 0, 1024);

1.3 读写操作

每种基本 Java 类型都有一种缓冲区类型,以 ByteBuffer 类为例,可以在其底层字节数组上进行 get、set 操作读写数据。

1)读操作
// 获取通道
FileInputStream fin = new FileInputStream( file );  
FileChannel fc = fin.getChannel();  
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate( 1024 );  
// 将数据从通道读到缓冲区中 
channel.read(buffer);
顺序访问
byte[] b = buffer.array();
//or
while (buffer.hasRemaining()){
    byte b = buffer.get();
}
随机访问
for ( int i=0;i < buffer.limit(); i++){
    byte b = buffer.get(i);
}
访问顺序

Java 对二进制数据使用高位在前的排序机制,但如需以低位在前处理:

buffer.order(ByteOrder.LITTLE_ENDIAN);

查询缓冲区内当前的字节顺序:

ByteOrder b = buffer.order();
2)写操作
// 获取通道
FileOutputStream fout = new FileOutputStream(file);
FileChannel channel = fout.getChannel();  
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate( 1024 );  
// 写入数据  
for (int i = 0; i < message.length; ++i) {  
    buffer.put( message[i] );  
}
// 反转缓冲区  
buffer.flip();  
// 写入缓冲区
channel.write( buffer );  

在恰当的时候,以及当通道关闭时,会将这些修改写回到文件中。

反转

buffer.flip();为读入后的输出做准备

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
  1. 将limit设置为当前position。
  2. 将position设置为0。
3)读写结合

拷贝

public static void fileCopyNIO(String source, String target) throws IOException {
     try (FileInputStream in = new FileInputStream(source)) {
            try (FileOutputStream out = new FileOutputStream(target)) {
                   FileChannel inChannel = in.getChannel();
                   FileChannel outChannel = out.getChannel();
                   ByteBuffer buffer = ByteBuffer.allocate(4096);
                   while (inChannel.read(buffer) != -1) {
                         buffer.flip();
                         outChannel.write(buffer);
                         buffer.clear();
                   }
            }
     }
}
重设缓冲区

buffer.clear();初始化缓冲区

public final Buffer clear() {
    osition = 0;
    limit = capacity;
    mark = -1;
    return this;
}
  1. 将 limit 设置为与 capacity 相同。
  2. 设置 position 为 0。

2 缓冲区数据结构

缓冲区是由具有相同类型的数值构成的数组,有复杂的内部统计机制,会跟踪已经读了多少数据以及还有多少空间可以容纳更多的数据。

缓冲区有两个重要组件:状态变量和访问方法。实现机制看起来复杂,但大都封装好了,只需像平时使用字节数组和索引变量一样进行操作即可。

2.1 状态变量

如图,每个缓冲区都具有:

这些值满足: 0<= mark <= position <= limit <= capacity

每一个基本类型的缓冲区底层实际上就是一个该类型的数组。如在 ByteBuffer 中,有:

final byte[] hb;  

2.2 访问方法

使用缓冲区的主要目的是执行“写,然后读入”循环。

  1. 假设有一个缓冲区,初始位置为 0,界限等于容量。
  2. 不断地调用 put 将值添加到这个缓冲区中,当耗尽所有的数据或者写出的数据量达到容量大小时,就该切换到读入操作了。
  3. 调用 flip 方法将界限设置到当前位置,并把位置复位到 0。
  4. 在 remaining 方法返回正数时(它返回的值是“界限-位置”),不断地调用 get。
  5. 在缓冲区的值都读入之后,调用 clear 使缓冲区为下一次写循环做好准备。clear 方法将位置复位到 0,并将界限复位到容量。

如果想重读缓冲区,可以使用 rewined 或 mark/reset 方法。

要获取缓冲区,可以调用诸如 ByteBuffer.allocate 或 ByteBuffer.wrap 这样的静态方法。
然后,可以用来自某个通道的数据填充缓冲区,或者将缓冲区的内容写出通道中。

ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);

3 文件加锁机制

文件锁可以解决并发修改同一个文件的问题,它可以控制对文件或文件中某个范围的字节的访问。

3.1 锁定整个文件

1)lock

获得独占锁,阻塞直至获得锁。

FileChannel = FileChannel.open(path);
FileLock lock = channel.lock();
2)tryLock

获得独占锁,在无法获得的情况下返回 null。

FileLock lock = channel.rtyLock();

3.2 锁定部分文件

在文件区域上获得锁,阻塞直至获得锁。

FileLock lock (long start,long size,boolean shared)

在文件区域上获得锁,无法获得锁时返回 null。

FileLock tryLock(long start,long size,boolean shared)
1)锁类型

调用 FileLock 类的 isShared 方法可以查询所持有的锁的类型。

2)锁区间

如果锁定了文件的尾部,而这个文件的长度随后增长了超过锁定的部分,那么增长出来的额外区域是未锁定的,要想锁定所有字节,可以使用Long.MAX_VALUE来表示尺寸。

3.3 释放锁

try(FileLock lock = channel.lock()){
    access the locked file or segment
}

3.4 注意

文件加锁机制时依赖操作系统的