热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

JavaNIO文件通道FileChannel用法及原理

这篇文章主要介绍了JavaNIO文件通道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 

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 

本例中客户端使用的是阻塞模式,服务端通道关闭输出(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内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 处理docker容器时间和宿主机时间不一致问题的方法
    本文介绍了处理docker容器时间和宿主机时间不一致问题的方法,包括复制主机的localtime到容器、处理报错情况以及重启容器的步骤。通过这些方法,可以解决docker容器时间和宿主机时间不一致的问题。 ... [详细]
  • 集合的遍历方式及其局限性
    本文介绍了Java中集合的遍历方式,重点介绍了for-each语句的用法和优势。同时指出了for-each语句无法引用数组或集合的索引的局限性。通过示例代码展示了for-each语句的使用方法,并提供了改写为for语句版本的方法。 ... [详细]
  • Python SQLAlchemy库的使用方法详解
    本文详细介绍了Python中使用SQLAlchemy库的方法。首先对SQLAlchemy进行了简介,包括其定义、适用的数据库类型等。然后讨论了SQLAlchemy提供的两种主要使用模式,即SQL表达式语言和ORM。针对不同的需求,给出了选择哪种模式的建议。最后,介绍了连接数据库的方法,包括创建SQLAlchemy引擎和执行SQL语句的接口。 ... [详细]
  • position属性absolute与relative的区别和用法详解
    本文详细解读了CSS中的position属性absolute和relative的区别和用法。通过解释绝对定位和相对定位的含义,以及配合TOP、RIGHT、BOTTOM、LEFT进行定位的方式,说明了它们的特性和能够实现的效果。同时指出了在网页居中时使用Absolute可能会出错的原因,即以浏览器左上角为原始点进行定位,不会随着分辨率的变化而变化位置。最后总结了一些使用这两个属性的技巧。 ... [详细]
  • 开发笔记:Docker 上安装启动 MySQL
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了Docker上安装启动MySQL相关的知识,希望对你有一定的参考价值。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 本文介绍了Java的公式汇总及相关知识,包括定义变量的语法格式、类型转换公式、三元表达式、定义新的实例的格式、引用类型的方法以及数组静态初始化等内容。希望对读者有一定的参考价值。 ... [详细]
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • 本文介绍了一种图片处理应用,通过固定容器来实现缩略图的功能。该方法可以实现等比例缩略、扩容填充和裁剪等操作。详细的实现步骤和代码示例在正文中给出。 ... [详细]
  • C++语言入门:数组的基本知识和应用领域
    本文介绍了C++语言的基本知识和应用领域,包括C++语言与Python语言的区别、C++语言的结构化特点、关键字和控制语句的使用、运算符的种类和表达式的灵活性、各种数据类型的运算以及指针概念的引入。同时,还探讨了C++语言在代码效率方面的优势和与汇编语言的比较。对于想要学习C++语言的初学者来说,本文提供了一个简洁而全面的入门指南。 ... [详细]
  • 本文介绍了H5游戏性能优化和调试技巧,包括从问题表象出发进行优化、排除外部问题导致的卡顿、帧率设定、减少drawcall的方法、UI优化和图集渲染等八个理念。对于游戏程序员来说,解决游戏性能问题是一个关键的任务,本文提供了一些有用的参考价值。摘要长度为183字。 ... [详细]
  • 本文介绍了Python函数的定义与调用的方法,以及函数的作用,包括增强代码的可读性和重用性。文章详细解释了函数的定义与调用的语法和规则,以及函数的参数和返回值的用法。同时,还介绍了函数返回值的多种情况和多个值的返回方式。通过学习本文,读者可以更好地理解和使用Python函数,提高代码的可读性和重用性。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
author-avatar
木木爱上林
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有