Java NIO 文件通道 FileChannel 用法及原理

FileChannel 提供了一种通过通道来访问文件的方式,它可以通过带参数 position(int) 方法定位到文件的任意位置开始进行操作,还能够将文件映射到直接内存,提高大文件的访问效率。本文将介绍其详细用法和原理。

1. 通道获取

FileChannel 可以通过 FileInputStream, FileOutputStream, RandomAccessFile 的对象中的 getChannel() 方法来获取,也可以同通过静态方法 FileChannel.open(Path, OpenOption ...) 来打开。

1.1 从 FileInputStream / FileOutputStream 中获取

从 FileInputStream 对象中获取的通道是以读的方式打开文件,从 FileOutpuStream 对象中获取的通道是以写的方式打开文件。

FileOutputStream ous = new FileOutputStream(new File("a.txt"));
FileChannel out = ous.getChannel(); // 获取一个只读通道
FileInputStream ins = new FileInputStream(new File("a.txt"));
FileChannel in = ins.getChannel(); // 获取一个只写通道

1.2 从 RandomAccessFile 中获取

从 RandomAccessFaile 中获取的通道取决于 RandomAccessFaile 对象是以什么方式创建的,"r", "w", "rw" 分别对应着读模式,写模式,以及读写模式。

RandomAccessFile file = new RandomAccessFile("a.txt", "rw");
FileChannel channel = file.getChannel(); // 获取一个可读写文件通道

1.3 通过 FileChannel.open() 打开

通过静态静态方法 FileChannel.open() 打开的通道可以指定打开模式,模式通过 StandardOpenOption 枚举类型指定。

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ); // 以只读的方式打开一个文件 a.txt 的通道

2. 读取数据

读取数据的 read(ByteBuffer buf) 方法返回的值表示读取到的字节数,如果读到了文件末尾,返回值为 -1。读取数据时,position 会往后移动。

2.1 将数据读取到单个缓冲区

和一般通道的操作一样,数据也是需要读取到1个缓冲区中,然后从缓冲区取出数据。在调用 read 方法读取数据的时候,可以传入参数 position 和 length 来指定开始读取的位置和长度。

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
ByteBuffer buf = ByteBuffer.allocate(5);
while(channel.read(buf)!=-1){
 buf.flip();
 System.out.print(new String(buf.array()));
 buf.clear();
}
channel.close();

2.2 读取到多个缓冲区

文件通道 FileChannel 实现了 ScatteringByteChannel 接口,可以将文件通道中的内容同时读取到多个 ByteBuffer 当中,这在处理包含若干长度固定数据块的文件时很有用。

ScatteringByteChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
ByteBuffer key = ByteBuffer.allocate(5), value=ByteBuffer.allocate(10);
ByteBuffer[] buffers = new ByteBuffer[]{key, value};
while(channel.read(buffers)!=-1){
 key.flip();
 value.flip();
 System.out.println(new String(key.array()));
 System.out.println(new String(value.array()));
 key.clear();
 value.clear();
}
channel.close();

3. 写入数据

3.1 从单个缓冲区写入

单个缓冲区操作也非常简单,它返回往通道中写入的字节数。

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
ByteBuffer buf = ByteBuffer.allocate(5);
byte[] data = "Hello, Java NIO.".getBytes();
for (int i = 0; i < data.length; ) {
 buf.put(data, i, Math.min(data.length - i, buf.limit() - buf.position()));
 buf.flip();
 i += channel.write(buf);
 buf.compact();
}
channel.force(false);
channel.close();

3.2 从多个缓冲区写入

FileChannel 实现了 GatherringByteChannel 接口,与 ScatteringByteChannel 相呼应。可以一次性将多个缓冲区的数据写入到通道中。

FileChannel channel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
ByteBuffer key = ByteBuffer.allocate(10), value = ByteBuffer.allocate(10);
byte[] data = "017 Robothy".getBytes();
key.put(data, 0, 3);
value.put(data, 4, data.length-4);
ByteBuffer[] buffers = new ByteBuffer[]{key, value};
key.flip();
value.flip();
channel.write(buffers);
channel.force(false); // 将数据刷出到磁盘
channel.close();

3.3 数据刷出

为了减少访问磁盘的次数,通过文件通道对文件进行操作之后可能不会立即刷出到磁盘,此时如果系统崩溃,将导致数据的丢失。为了减少这种风险,在进行了重要数据的操作之后应该调用 force() 方法强制将数据刷出到磁盘。

无论是否对文件进行过修改操作,即使文件通道是以只读模式打开的,只要调用了 force(metaData) 方法,就会进行一次 I/O 操作。参数 metaData 指定是否将元数据(例如:访问时间)也刷出到磁盘。

channel.force(false); // 将数据刷出到磁盘,但不包括元数据

4. 文件锁

可以通过调用 FileChannel 的 lock() 或者 tryLock() 方法来获得一个文件锁,获取锁的时候可以指定参数起始位置 position,锁定大小 size,是否共享 shared。如果没有指定参数,默认参数为 position = 0, size = Long.MAX_VALUE, shared = false。

位置 position 和大小 size 不需要严格与文件保持一致,position 和 size 均可以超过文件的大小范围。例如:文件大小为 100,可以指定位置为 200, 大小为 50;则当文件大小扩展到 250 时,[200,250) 的部分会被锁住。

shared 参数指定是排他的还是共享的。要获取共享锁,文件通道必须是可读的;要获取排他锁,文件通道必须是可写的。

由于 Java 的文件锁直接映射为操作系统的文件锁实现,因此获取文件锁时代表的是整个虚拟机,而非当前线程。若操作系统不支持共享的文件锁,即使指定了文件锁是共享的,也会被转化为排他锁。

FileLock lock = channel.lock(0, Long.MAX_VALUE, false);// 排它锁,此时同一操作系统下的其它进程不能访问 a.txt
System.out.println("Channel locked in exclusive mode.");
Thread.sleep(30 * 1000L); // 锁住 30 s
lock.release(); // 释放锁

lock = channel.lock(0, Long.MAX_VALUE, true); // 共享锁,此时文件可以被其它文件访问
System.out.println("Channel locked in shared mode.");
Thread.sleep(30 * 1000L); // 锁住 30 s
lock.release();

与 lock() 相比,tryLock() 是非阻塞的,无论是否能够获取到锁,它都会立即返回。若 tryLock() 请求锁定的区域已经被操作系统内的其它的进程锁住了,则返回 null;而 lock() 会阻塞,直到获取到了锁、通道被关闭或者线程被中断为止。

5. 通道转换

普通的读写方式是利用一个 ByteBuffer 缓冲区,作为数据的容器。但如果是两个通道之间的数据交互,利用缓冲区作为媒介是多余的。文件通道允许从一个 ReadableByteChannel 中直接输入数据,也允许直接往 WritableByteChannel 中写入数据。实现这两个操作的分别为 transferFrom(ReadableByteChannel src, position, count) 和 transferTo(position, count, WritableChannel target) 方法。

这进行通道间的数据传输时,这两个方法比使用 ByteBuffer 作为媒介的效率要高;很多操作系统支持文件系统缓存,两个文件之间实际可能并没有发生复制。

transferFrom 或者 transferTo 在调用之后并不会改变 position 的位置。

下面示例是一个 spring 源码中的一个工具方法。

public static void copy(File source, File target) throws IOException {
 FileInputStream sourceOutStream = new FileInputStream(source);
 FileOutputStream targetOutStream = new FileOutputStream(target);
 FileChannel sourceChannel = sourceOutStream.getChannel();
 FileChannel targetChannel = targetOutStream.getChannel();
 sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
 sourceChannel.close();
 targetChannel.close();
 sourceOutStream.close();
 targetOutStream.close();
}

需要注意的是,调用这两个转换方法之后,某些情况下并不保证数据能够全部完成传输,确切传输了多少字节的数据需要根据返回的值来进行判断。例如:从一个非阻塞模式下的 SocketChannel 中输入数据就不能够一次性将数据全部传输过来,或者将文件通道的数据传输给一个非阻塞模式下的 SocketChannel 不能一次性传输过去。

下面给出一个示例,客户端连接到服务端,然后从服务端下载一个叫 video.mp4 文件,文件在当前目录存在。

错误示例:

/** 服务端 **/
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 打开服务通道
serverSocketChannel.bind(new InetSocketAddress(9090)); // 绑定端口号
SocketChannel clientChannel = serverSocketChannel.accept(); // 等待客户端连接,获取 SocketChannel
FileChannel fileChannel = FileChannel.open(Paths.get("video.mp4"), StandardOpenOption.READ); // 打开文件通道
fileChannel.transferTo(0, fileChannel.size(), clientChannel); // 【可能出错位置】文件通道数据输出转化到 socket 通道,输出范围为整个文件。文件太大将导致输出不完整

/** 客户端 **/
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9090)); // 打卡 socket 通道并连接到服务端
FileChannel fileChannel = FileChannel.open(Paths.get("video-downloaded.mp4"), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE, StandardOpenOption.CREATE); // 打开文件通道
fileChannel.transferFrom(socketChannel, 0, Long.MAX_VALUE); // 【非阻塞模式下可能出错】
fileChannel.force(false); // 确保数据刷出到磁盘

正确的姿势是:transferTo/transferFrom 的时候应该用一个循环检查实际输出内容大小是否和期望输出内容大小一致,特别是通道处于非阻塞模式下,极大概率不能够一次传输完成。

所以服务端正确的转换方式是:

long transfered = 0;
while (transfered < fileChannel.size()){
 transfered += fileChannel.transferTo(transfered, fileChannel.size(), clientChannel);
}

本例中客户端使用的是阻塞模式,服务端通道关闭输出(socketChannel.shutdownOutput())之后 transferFrom 才退出,服务端正常关闭通道的情况下数据传输不会出错,这里就不处理非正常关闭的情况了。(完整代码)。

6. 截取文件

FileChannel.truncate(long size) 可以截取指定的文件,指定大小之后的内容将被丢弃。size 的值可以超过文件大小,超过的话不会截取任何内容,也不会增加任何内容。

FileChannel fileChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE);
fileChannel.truncate(1);
System.out.println(fileChannel.size()); // 输出 1
fileChannel.write(ByteBuffer.wrap("Hello".getBytes()));
System.out.println(fileChannel.size()); // 输出 5
fileChannel.force(true);
fileChannel.close();

7. 映射文件到直接内存

文件通道 FileChannel 可以将文件的指定范围映射到程序的地址空间中,映射部分使用字节缓冲区的一个子类 MappedByteBuffer 的对象表示,只要对映射字节缓冲区进行操作就能够达到操作文件的效果。与之相对应的,前面介绍的内容是通过操作文件通道和堆内存中的字节缓冲区 HeapByteBuffer 来达到操作文件的目的。

通过 ByteBuffer.allocate() 分配的缓冲区是一个 HeapByteBuffer,存在于 JVM 堆中;而 FileChannle.map() 将文件映射到直接内存,返回的是一个 MappedByteBuffer,存在于堆外的直接内存中;这块内存在 MappedByteBuffer 对象本身被回收之前有效。

7.1 内存映射原理

前面使用堆缓冲区 ByteBuffer 和文件通道 FileChannel 对文件的操作使用的是 read()/write() 系统调用。读取数据时数据从 I/O 设备读到内核缓存,再从内核缓存复制到用户空间缓存,这里是 JVM 的堆内存。而映射磁盘文件是使用 mmap() 系统调用,将文件的指定部分映射到程序地址空间中;数据交互发生在 I/O 设备于用户空间之间,不需要经过内核空间。

虽然映射磁盘文件减少了一次数据复制,但对于大多数操作系统来说,将文件映射到内存这个操作本身开销较大;如果操作的文件很小,只有数十KB,映射文件所获得的好处将不及其开销。因此,只有在操作大文件的时候才将其映射到直接内存。

7.2 映射缓冲区用法

文件通道 FileChanle 通过成员方法 map(MapMode mode, long position, long size) 将文件映射到应用内存。

FileChannel fileChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE); // 以读写的方式打开文件通道
MappedByteBuffer buf = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size()); // 将整个文件映射到内存

mode 表示打开模式,为枚举值,其值可以为 READ_ONLY, READ_WRITE, PRIVATE。
+ 模式为 READ_ONLY 时,不能对 buf 进行写操作;
+ 模式为 READ_WRITE 时,通道 fileChannel 必须具有读写文件的权限;对 buf 进行的写操作将对文件生效,但不保证立即同步到 I/O 设备;
+ 模式为 PRIVATE 时,通道 fileChannle 必须对文件有读写权限;但是对文件的修改操作不会传播到 I/O 设备,而是会在内存复制一份数据。此时对文件的修改对其它线程和进程不可见。

position 指定文件的开始映射到内存的位置;

size 指定映射的大小,值为非负 int 型整数。

调用 map() 方法之后,返回的 MappedByteBuffer 就于 fileChannel 脱离了关系,关闭 fileChannel 对 buf 没有影响。同时,如果要确保对 buf 修改的数据能够同步到文件 I/O 设备中,需要调用 MappedByteBuffer 中的无参数的 force() 方法,而调用 FileChannel 中的 force(metaData) 方法无效。

此时可以通过操作缓冲区来操作文件了。不过映射的内容存在于 JVM 程序的堆外内存中,这部分内存是虚拟内存,意味着 buf 中的内容不一定都在物理内存中,要让这些内容加载到物理内存,可以调用 MappedByteBuffer 中的 load() 方法。另外,还可以调用 isLoaded() 来判断 buf 中的内容是否在物理内存中。

FileChannel fileChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ);
MappedByteBuffer buf = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size());
fileChannel.close(); // 关于文件通道对 buf 没有影响
System.out.println(buf.capacity()); // 输出 fileChannel.size()
System.out.println(buf.limit()); // 输出 fileChannel.size()
System.out.println(buf.position()); // 输出 0
buf.put((byte)'R'); // 写入内容
buf.compact();  // 截掉 positoin 之前的内容
buf.force();  // 将数据刷出到 I/O 设备

8. 小结

1)文件通道 FileChannel 能够将数据从 I/O 设备中读入(read)到字节缓冲区中,或者将字节缓冲区中的数据写入(write)到 I/O 设备中。

2)文件通道能够转换到 (transferTo) 一个可写通道中,也可以从一个可读通道转换而来(transferFrom)。这种方式使用于通道之间地数据传输,比使用缓冲区更加高效。

3)文件通道能够将文件的部分内容映射(map)到 JVM 堆外内存中,这种方式适合处理大文件,不适合处理小文件,因为映射过程本身开销很大。

4)在对文件进行重要的操作之后,应该将数据刷出刷出(force)到磁盘,避免操作系统崩溃导致的数据丢失。

到此这篇关于Java NIO 文件通道 FileChannel 用法的文章就介绍到这了,更多相关Java NIO 文件通道 FileChannel内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java NIO异步文件通道原理及用法解析

    在Java 7,AsynchronousFileChannel 被添加到了Java NIO中.使用AsynchronousFileChannel可以实现异步地读取和写入文件数据. 创建一个AsynchronousFileChannel 我们可以使用AsynchronousFileChannel提供的静态方法 open() 创建它.示例代码如下: Path path = Paths.get("data/test.xml"); AsynchronousFileChannel fileCha

  • Java NIO 文件通道 FileChannel 用法及原理

    FileChannel 提供了一种通过通道来访问文件的方式,它可以通过带参数 position(int) 方法定位到文件的任意位置开始进行操作,还能够将文件映射到直接内存,提高大文件的访问效率.本文将介绍其详细用法和原理. 1. 通道获取 FileChannel 可以通过 FileInputStream, FileOutputStream, RandomAccessFile 的对象中的 getChannel() 方法来获取,也可以同通过静态方法 FileChannel.open(Path, Op

  • 详解java NIO之Channel(通道)

    通道(Channel)是java.nio的第二个主要创新.它们既不是一个扩展也不是一项增强,而是全新.极好的Java I/O示例,提供与I/O服务的直接连接.Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据. channel介绍 通道是访问I/O服务的导管.I/O可以分为广义的两大类别:File I/O和Stream I/O.那么相应地有两种类型的通道也就不足为怪了,它们是文件(file)通道和套接字(socket)通道.我们看到在api里有一个F

  • java NIO 详解

    Java NIO提供了与标准IO不同的IO工作方式: Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中. Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情.当数据被写入到缓冲区时,线程可以继续处理它.从缓冲区写入通道也类似. S

  • Java中BIO、NIO和AIO的区别、原理与用法

    目录 IO BIO NIO AIO 区别及联系 各自适用场景 使用方式 IO 什么是IO? 它是指计算机与外部世界或者一个程序与计算机的其余部分的之间的接口.它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的.单独的程序一般是让系统为它们完成大部分的工作. 在 Java 编程中,直到最近一直使用 流 的方式完成 I/O.所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节.流 I/O 用于与外部世界接触.它也在内部使用,用于将

  • Java NIO Selector用法详解【含多人聊天室实例】

    本文实例讲述了Java NIO Selector用法.分享给大家供大家参考,具体如下: 一.Java NIO 的核心组件 Java NIO的核心组件包括:Channel(通道),Buffer(缓冲区),Selector(选择器),其中Channel和Buffer比较好理解 简单来说 NIO是面向通道和缓冲区的,意思就是:数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中. 关于Channel 和 Buffer的详细讲解请看:Java NIO 教程 二.Java NIO Se

  • Java NIO Buffer实现原理详解

    目录 1.Buffer的继承体系 2.Buffer的操作API使用案例 3.Buffer的基本原理 4.allocate方法初始化一个指定容量大小的缓冲区 5.slice方法缓冲区分片 6.只读缓冲区 7.直接缓冲区 8.内存映射 1.Buffer的继承体系 如上图所示,对于Java中的所有基本类型,都会有一个具体的Buffer类型与之对应,一般我们最经常使用的是ByteBuffer. 2.Buffer的操作API使用案例 举一个IntBuffer的使用案例: /** * @author csp

  • Java NIO工作原理的全面分析

    ◆  输入/输出:概念性描述I/O 简介I/O ? 或者输入/输出 ? 指的是计算机与外部世界或者一个程序与计算机的其余部分的之间的接口.它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内置在操作系统中的.单独的程序一般是让系统为它们完成大部分的工作.在 Java 编程中,直到最近一直使用 流 的方式完成 I/O.所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节.流 I/O 用于与外部世界接触.它也在内部使用,用于将对象转换为字节,然后再

  • Java NIO原理图文分析及代码实现

    前言: 最近在分析hadoop的RPC(Remote Procedure Call Protocol ,远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.可以参考:http://baike.baidu.com/view/32726.htm )机制时,发现hadoop的RPC机制的实现主要用到了两个技术:动态代理(动态代理可以参考博客:http://weixiaolu.iteye.com/blog/1477774 )和java NIO.为了能够正确地分析

  • Java NIO写大文件对比(win7和mac)

    测试说明 写2G文件,分批次写入,每批次写入128MB: 分别在Win7系统(3G内存,双核,32位,T系列处理器)和MacOS系统(8G内存,四核,64位,i7系列处理器)下运行测试.理论上跟硬盘类型和配置也有关系,这里不再贴出了. 测试代码 package rwbigfile; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.RandomA

随机推荐