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

1. 通道获取

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

1.1 从 FileInputStream / FileOutputStream 中获取

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

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

1.2 从 RandomAccessFile 中获取

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

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

1.3 通过 FileChannel.open() 打开

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

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

2. 读取数据

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

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

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

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

2.2 读取到多个缓冲区

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

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

3. 写入数据

3.1 从单个缓冲区写入

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

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

3.2 从多个缓冲区写入

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

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

3.3 数据刷出

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

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

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

  1. FileLock lock = channel.lock(0, Long.MAX_VALUE, false);// 排它锁,此时同一操作系统下的其它进程不能访问 a.txt
  2. System.out.println("Channel locked in exclusive mode.");
  3. Thread.sleep(30 * 1000L); // 锁住 30 s
  4. lock.release(); // 释放锁
  5. lock = channel.lock(0, Long.MAX_VALUE, true); // 共享锁,此时文件可以被其它文件访问
  6. System.out.println("Channel locked in shared mode.");
  7. Thread.sleep(30 * 1000L); // 锁住 30 s
  8. 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 源码中的一个工具方法。

  1. public static void copy(File source, File target) throws IOException {
  2. FileInputStream sourceOutStream = new FileInputStream(source);
  3. FileOutputStream targetOutStream = new FileOutputStream(target);
  4. FileChannel sourceChannel = sourceOutStream.getChannel();
  5. FileChannel targetChannel = targetOutStream.getChannel();
  6. sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
  7. sourceChannel.close();
  8. targetChannel.close();
  9. sourceOutStream.close();
  10. targetOutStream.close();
  11. }

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

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

错误示例:

  1. /** 服务端 **/
  2. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 打开服务通道
  3. serverSocketChannel.bind(new InetSocketAddress(9090)); // 绑定端口号
  4. SocketChannel clientChannel = serverSocketChannel.accept(); // 等待客户端连接,获取 SocketChannel
  5. FileChannel fileChannel = FileChannel.open(Paths.get("video.mp4"), StandardOpenOption.READ); // 打开文件通道
  6. fileChannel.transferTo(0, fileChannel.size(), clientChannel); // 【可能出错位置】文件通道数据输出转化到 socket 通道,输出范围为整个文件。文件太大将导致输出不完整
  7. /** 客户端 **/
  8. SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9090)); // 打卡 socket 通道并连接到服务端
  9. FileChannel fileChannel = FileChannel.open(Paths.get("video-downloaded.mp4"), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE, StandardOpenOption.CREATE); // 打开文件通道
  10. fileChannel.transferFrom(socketChannel, 0, Long.MAX_VALUE); // 【非阻塞模式下可能出错】
  11. fileChannel.force(false); // 确保数据刷出到磁盘

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

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

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

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

6. 截取文件

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

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

7. 映射文件到直接内存

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

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

.st11 {fill:#191919;font-family:72 Condensed;font-size:9pt}
主存主存JVM进程内存HeapByteBufferJVM 堆内存a) HeapByteBuffer 在内存中的位置b) MappedByteBuffer 在内存中的位置JVM 堆内存JVM进程内存MappedByteBuffer

7.1 内存映射原理

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

.st1 {fill:#191919;font-family:72 Condensed;font-size:9pt}
文件内核空间用户空间缓存IO设备缓存文件内核空间用户空间缓存IO设备缓存a) 普通 I/Ob) 内存映射 I/O

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

7.2 映射缓冲区用法

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

  1. FileChannel fileChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE); // 以读写的方式打开文件通道
  2. 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 中的内容是否在物理内存中。

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

8. 小结

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

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

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

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

Java NIO 文件通道 FileChannel 用法的更多相关文章

  1. Java NIO 文件通道使用

    读取一个文件的内容,然后写入另外一个文件 public class NioTest4 { public static void main(String[] args) throws Exception ...

  2. Java IO和Java NIO 和通道 在文件拷贝上的性能差异分析

    1.  在JAVA传统的IO系统中,读取磁盘文件数据的过程如下: 以FileInputStream类为例,该类有一个read(byte b[])方法,byte b[]是我们要存储读取到用户空间的缓冲区 ...

  3. Java NIO Channel之FileChannel [ 转载 ]

    Java NIO Channel之FileChannel [ 转载 ] @author zachary.guo 对于文件 I/O,最强大之处在于异步 I/O(asynchronous I/O),它允许 ...

  4. Java NIO Channel通道

    原文链接:http://tutorials.jenkov.com/java-nio/channels.html Java NIO Channel通道和流非常相似,主要有以下几点区别: 通道可以读也可以 ...

  5. Java使用文件通道复制文件

    两种文件通道复制文件方式的性能比较 import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IO ...

  6. Java NIO之通道

    一.前言 前面学习了缓冲区的相关知识点,接下来学习通道. 二.通道 2.1 层次结构图 对于通道的类层次结构如下图所示. 其中,Channel是所有类的父类,其定义了通道的基本操作.从 Channel ...

  7. 【NIO】Java NIO之通道

    一.前言 前面学习了缓冲区的相关知识点,接下来学习通道. 二.通道 2.1 层次结构图 对于通道的类层次结构如下图所示. 其中,Channel是所有类的父类,其定义了通道的基本操作.从 Channel ...

  8. JAVA NIO 文件部分

    NIO java使用NIO的目的是为了提升性能,实际上老的io程序也已经优化过了,速度也有相应的提升. NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector.传 ...

  9. Java NIO:通道

    最近打算把Java网络编程相关的知识深入一下(IO.NIO.Socket编程.Netty) Java NIO主要需要理解缓冲区.通道.选择器三个核心概念,作为对Java I/O的补充, 以提升大批量数 ...

随机推荐

  1. NET CORE通过NodeService调用js

    在 .NET Framework 时,我们可以通过V8.NET等组件来运行 JavaScript,不过目前我看了好几个开源组件包括V8.NET都还不支持 .NET Core ,我们如何在 .NET C ...

  2. 【题解】NOI 系列题解总集

    每次做一道 NOI 系列的估计都很激动吧,对于我这种萌新来说( P1731 [NOI1999]生日蛋糕 练习剪枝技巧,关于剪枝,欢迎看我的垃圾无意义笔记 这道题是有一定难度的,需要运用各种高科技剪枝( ...

  3. 惊天秘密!如何在 Flutter 项目中实现操作引导

    不要冒然评价我,你只知道我的名字,却不知道我的故事,你只是听闻我做了什么,却不知我经历过什么. 俗话说得好,产品有三宝,弹窗浮层加引导. 上图截图自我司 App 晓黑板中的口算模块,相信每个 App ...

  4. Python搭建调用本地dll的Windows服务(浏览器可以访问,附测试dll64位和32位文件)

    一.前言说明 博客声明:此文链接地址https://www.cnblogs.com/Vrapile/p/14113683.html,请尊重原创,未经允许禁止转载!!! 1. 功能简述 (1)本文提供生 ...

  5. ssh远程服务器不通

    1.关闭防火墙 service iptables status service iptables stop 2.在/etc/hosts文件添加远程服务器信息(连接的两端都添加) 服务器1(racdb1 ...

  6. AWT09-弹球小游戏

    1.补充 为了让Java绘制动画,可以借助Swing的定时器,其构造为: 方法名 说明 Timer(int delay,ActionListener listener) 每间隔delay毫秒,自动触发 ...

  7. 安卓和ios的app证书过期的相关问题汇总

    一,ios的APP的发布流程请见:ios的APP的发布流程 http://www.jianshu.com/p/b1b77d804254 这篇文章写得很好很全面 二,app证书过期了怎么办: IOS的情 ...

  8. css进阶 03-网页设计和开发中,关于字体的常识

    03-网页设计和开发中,关于字体的常识 #前言 我周围的码农当中,有很多是技术大神,却常常被字体这种简单的东西所困扰. 这篇文章,我们来讲一讲关于字体的常识.这些常识所涉及到的问题,有很强的可操作性, ...

  9. [BUUOJ]刮开有奖reverse

    刮开有奖 这是一个赌博程序,快去赚钱吧!!!!!!!!!!!!!!!!!!!!!!!!!!!(在编辑框中的输入值,即为flag,提交即可) 注意:得到的 flag 请包上 flag{} 提交 1.查壳 ...

  10. Erlang那些事儿第1回之我是变量,一次赋值永不改变

    第1回先从不变的变量说开来,学过其他编程语言的人都知道,变量之所以叫变量,是因为它会经常变,被修改.假设原本X  = 10,后来再执行X = 24,那么X就从10变成了24,这对于程序新手和老鸟来说, ...