1.简介

通道是 Java NIO 的核心内容之一,在使用上,通道需和缓存类(ByteBuffer)配合完成读写等操作。与传统的流式 IO 中数据单向流动不同,通道中的数据可以双向流动。通道既可以读,也可以写。这里我们举个例子说明一下,我们可以把通道看做水管,把缓存看做水塔,把文件看做水库,把水看做数据。当从磁盘中将文件数据读取到缓存中时,就是从水库向水塔里抽水。当然,从磁盘里读取数据并不会将读取的部分从磁盘里删除,但从水库里抽水,则水库里的水量在无补充的情况下确实变少了。当然,这只是一个小问题,大家不要扣这个细节哈,继续往下说。当水塔中存储了水之后,我们可以用这些水烧饭,浇花等,这就相当于处理缓存的数据。过了一段时间后,水塔需要进行清洗。这个时候需要把水塔里的水放回水库中,这就相当于向磁盘中写入数据。通过这里例子,大家应该知道通道是什么了,以及有什么用。既然知道了,那么我们继续往下看。

Java NIO 出现在 JDK 1.4 中,由于 NIO 效率高于传统的 IO,所以 Sun 公司从底层对传统 IO 的实现进行了修改。修改的方式就是在保证兼容性的情况下,使用 NIO 重构 IO 的方法实现,无形中提高了传统 IO 的效率。

2.基本操作

通道类型分为两种,一种是面向文件的,另一种是面向网络的。具体的类声明如下:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

正如上列表,NIO 通道涵盖了文件 IO,TCP 和 UDP 网络 IO 等通道类型。本文我们先来说说文件通道。

2.1 创建通道

FileChannel 是一个用于连接文件的通道,通过该通道,既可以从文件中读取,也可以向文件中写入数据。与SocketChannel 不同,FileChannel 无法设置为非阻塞模式,这意味着它只能运行在阻塞模式下。在使用FileChannel 之前,需要先打开它。由于 FileChannel 是一个抽象类,所以不能通过直接创建而来。必须通过像 InputStream、OutputStream 或 RandomAccessFile 等实例获取一个 FileChannel 实例。

1
2
3
4
5
6
7
8
FileInputStream fis = new FileInputStream(FILE_PATH);
FileChannel channel = fis.getChannel();FileOutputStream fos = new FileOutputStream(FILE_PATH);
FileChannel channel = fis.getChannel();RandomAccessFile raf = new RandomAccessFile(FILE_PATH , "rw");
FileChannel channel = raf.getChannel();

2.2 读写操作

读写操作比较简单,这里直接上代码了。下面的代码会先向文件中写入数据,然后再将写入的数据读出来并打印。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 获取管道
RandomAccessFile raf = new RandomAccessFile(FILE_PATH, "rw");
FileChannel rafChannel = raf.getChannel();// 准备数据
String data = "新数据,时间: " + System.currentTimeMillis();
System.out.println("原数据:\n" + "   " + data);
ByteBuffer buffer = ByteBuffer.allocate(128);
buffer.clear();
buffer.put(data.getBytes());
buffer.flip();// 写入数据
rafChannel.write(buffer);rafChannel.close();
raf.close();// 重新打开管道
raf = new RandomAccessFile(FILE_PATH, "rw");
rafChannel = raf.getChannel();// 读取刚刚写入的数据
buffer.clear();
rafChannel.read(buffer);// 打印读取出的数据
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
System.out.println("读取到的数据:\n" + "   " + new String(bytes));rafChannel.close();
raf.close();

上面的代码输出结果如下:

2.3 数据转移操作

我们有时需要将一个文件中的内容复制到另一个文件中去,最容易想到的做法是利用传统的 IO 将源文件中的内容读取到内存中,然后再往目标文件中写入。现在,有了 NIO,我们可以利用更方便快捷的方式去完成复制操作。FileChannel 提供了一对数据转移方法 - transferFrom/transferTo,通过使用这两个方法,即可简化文件复制操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void main(String[] args) throws IOException {RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");FileChannel fromChannel = fromFile.getChannel();RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");FileChannel toChannel = toFile.getChannel();long position = 0;long count = fromChannel.size();// 将 fromFile 文件找那个的数据转移到 toFile 中去System.out.println("before transfer: " + readChannel(toChannel));fromChannel.transferTo(position, count, toChannel);System.out.println("after transfer : " + readChannel(toChannel));fromChannel.close();fromFile.close();toChannel.close();toFile.close();
}private static String readChannel(FileChannel channel) throws IOException {ByteBuffer buffer = ByteBuffer.allocate(32);buffer.clear();// 将 channel 读取位置设为 0,也就是文件开始位置channel.position(0);channel.read(buffer);// 再次将文件位置归零channel.position(0);buffer.flip();byte[] bytes = new byte[buffer.limit()];buffer.get(bytes);return new String(bytes);
}

通过上面的代码,我们可以明显感受到,利用 transferTo 减少了编码量。那么为什么利用 transferTo 可以减少编码量呢?在解答这个问题前,先来说说程序读取数据和写入文件的过程。

我们现在所使用的 PC 操作系统,将内存分为了内核空间和用户空间。操作系统的内核和一些硬件的驱动程序就是运行在内核空间内,而用户空间就是我们自己写的程序所能运行的内存区域。这里,当我们调用 read 从磁盘中读取数据时,内核会首先将数据读取到内核空间中,然后再将数据从内核空间复制到用户空间内。也就是说,我们需要通过内核进行数据中转。同样,写入数据也是如此。系统先从用户空间将数据拷贝到内核空间中,然后再由内核空间向磁盘写入。相关示意图如下:

与上面的数据流向不同,FileChannel 的 transferTo 方法底层基于 sendfile64(Linux 平台下)系统调用实现。sendfile64 会直接在内核空间内进行数据拷贝,免去了内核往用户空间拷贝,用户空间再往内核空间拷贝这两步操作,因此提高了效率。其示意图如下:

通过上面的讲解,大家应该知道了 transferTo 和 transferFrom 的效率会高于传统的 read 和 write 在效率上的区别。区别的原因在于免去了内核空间和用户空间的相互拷贝,虽然内存间拷贝的速度比较快,但涉及到大量的数据拷贝时,相互拷贝的带来的消耗是不应该被忽略的。

讲完了背景知识,咱们再来看看 FileChannel 是怎样调用 sendfile64 这个函数的。相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public long transferTo(long position, long count,WritableByteChannel target)throws IOException
{// 省略一些代码int icount = (int)Math.min(count, Integer.MAX_VALUE);if ((sz - position) < icount)icount = (int)(sz - position);long n;// Attempt a direct transfer, if the kernel supports itif ((n = transferToDirectly(position, icount, target)) >= 0)return n;// Attempt a mapped transfer, but only to trusted channel typesif ((n = transferToTrustedChannel(position, icount, target)) >= 0)return n;// Slow path for untrusted targetsreturn transferToArbitraryChannel(position, icount, target);
}private long transferToDirectly(long position, int icount,WritableByteChannel target)throws IOException
{// 省略一些代码long n = -1;int ti = -1;try {begin();ti = threads.add();if (!isOpen())return -1;do {n = transferTo0(thisFDVal, position, icount, targetFDVal);} while ((n == IOStatus.INTERRUPTED) && isOpen());// 省略一些代码return IOStatus.normalize(n);} finally {threads.remove(ti);end (n > -1);}
}

从上面代码(transferToDirectly 方法可以在 openjdk/jdk/src/share/classes/sun/nio/ch/FileChannelImpl.java 中找到)中可以看得出 transferTo 的调用路径,先是调用 transferToDirectly,然后 transferToDirectly 再调用 transferTo0。transferTo0 是 native 类型的方法,我们再去看看 transferTo0 是怎样实现的,其代码在openjdk/jdk/src/solaris/native/sun/nio/ch/FileChannelImpl.c中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this,jint srcFD,jlong position, jlong count,jint dstFD)
{
#if defined(__linux__)off64_t offset = (off64_t)position;jlong n = sendfile64(dstFD, srcFD, &offset, (size_t)count);if (n < 0) {if (errno == EAGAIN)return IOS_UNAVAILABLE;if ((errno == EINVAL) && ((ssize_t)count >= 0))return IOS_UNSUPPORTED_CASE;if (errno == EINTR) {return IOS_INTERRUPTED;}JNU_ThrowIOExceptionWithLastError(env, "Transfer failed");return IOS_THROWN;}return n;// 其他平台的代码省略
#endif
}

如上所示,transferTo0 最终调用了 sendfile64 函数,关于 sendfile64 这个系统调用的详细说明,请参考 man-page,这里就不展开说明了。

2.4 内存映射

内存映射这个概念源自操作系统,是指将一个文件映射到某一段虚拟内存(物理内存可能不连续)上去。我们通过对这段虚拟内存的读写即可达到对文件的读写的效果,从而可以简化对文件的操作。当然,这只是内存映射的一个优点。内存映射还有其他的一些优点,比如两个进程映射同一个文件,可以实现进程间通信。再比如,C 程序运行时需要 C 标准库支持,操作系统将 C 标准库放到了内存中,普通的 C 程序只需要将 C 标准库映射到自己的进程空间内就行了,从而可以降低内存占用。以上简单介绍了内存映射的概念及作用,关于这方面的知识,建议大家去看《深入理解计算机系统》关于内存映射的章节,讲的很好。

Unix/Linux 操作系统内存映射的系统调用mmap,Java 在这个系统调用的基础上,封装了 Java 的内存映射方法。这里我就不一步一步往下追踪了,大家有兴趣可以自己追踪一下 Java 封装的内存映射方法的调用栈。下面来简单的示例演示一下内存映射的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 从标准输入获取数据
Scanner sc = new Scanner(System.in);
System.out.println("请输入:");
String str = sc.nextLine();
byte[] bytes = str.getBytes();RandomAccessFile raf = new RandomAccessFile("map.txt", "rw");
FileChannel channel = raf.getChannel();// 获取内存映射缓冲区,并向缓冲区写入数据
MappedByteBuffer mappedBuffer = channel.map(MapMode.READ_WRITE, 0, bytes.length);
mappedBuffer.put(bytes);raf.close();
raf.close();// 再次打开刚刚的文件,读取其中的内容
raf = new RandomAccessFile("map.txt", "rw");
channel = raf.getChannel();
System.out.println("\n文件内容:")
System.out.println(readChannel(channel));raf.close();
raf.close();

上面的代码从标准输入中获取数据,然后将数据通过内存映射缓存写入到文件中。代码运行结果如下:

接下来在用 C 代码演示上面代码的功能,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <memory.h>
#include <unistd.h>int main() {int dstfd;void *dst;char buf[64], out[64];int len;printf("Please input:\n");scanf("%s", buf);len = strlen(buf);// 打开文件dstfd = open("dst.txt", O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);lseek(dstfd, len - 1, SEEK_SET);write(dstfd, "", 1);// 将文件映射到内存中dst = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, dstfd, 0);// 将输入的数据拷贝到映射内存中memcpy(dst, buf, len);munmap(dst, len);close(dstfd);// 重新打开文件,并输出文件内容dstfd = open("dst.txt", O_RDONLY);dst = mmap(NULL, len, PROT_READ, MAP_SHARED, dstfd, 0);bzero(out, 64);memcpy(out, dst, len);printf("\nfile content:\n%s\n", out);munmap(dst, len);close(dstfd);return 0;
}

关于 mmap 函数的参数说明,这里就不细说了,大家可以参考 man-page。上面的代码运行结果如下:

关于内存映射就说到了,更深入的分析需要涉及到很多操作系统层面的东西。我对这些东西了解的也不多,所以就不继续分析了,惭愧惭愧。

2.5 其他操作

FileChannel 还有一些其他的方法,这里通过一个表格来列举这些方法,就不一一展开说明了。如下:

方法名 用途
position 返回或修改通道读写位置
size 获取通道所关联文件的大小
truncate 截断通道所关联的文件
force 强制将通道中的新数据刷新到文件中
close 关闭通道
lock 对通道文件进行加锁

以上所列举的方法用起来比较简单,大家自己写代码验证一下吧,这里就不贴代码了。

3.总结

以上章节对 NIO 文件通道的用法和部分方法的实现进行了简单分析。从上面的分析可以看出,NIO FileChannel 在实现上,实际上是对底层操作系统的一些 API 进行了再次封装,也就是一层皮。有了这层封装后,对上就屏蔽了底层 API 的细节,以降低使用难度。Java 为了提高开发效率,屏蔽了操作系统层面的细节。虽然 Java 可以屏蔽这些细节,但作为开发人员,我觉得我们不能也去屏蔽这些细节(虽然不了解这些细节也能写代码),有时间还是应该多了解了解这些底层的东西。毕竟要想往更高的层次发展,这些底层的知识必不可少。说到这里,感觉很惭愧,我的技术基础也很薄弱。大学期间没有意识到专业基础课的重要性,学了很多东西,但忽略了基础。好在工作不久后看了很多牛人的博客,也意识到了自己的不足。现在静下心来打基础,算是亡羊补牢吧。

好了,关于文件通道的内容这里就说到这,谢谢大家的阅读。

参考

  • 《Java 编程思想》
  • 《深入理解计算机系统》
  • Java NIO Channel
  • 本文链接: https://www.tianxiaobo.com/2018/03/24/JAVA-NIO之文件通道/

from:http://www.tianxiaobo.com/2018/03/24/JAVA-NIO%E4%B9%8B%E6%96%87%E4%BB%B6%E9%80%9A%E9%81%93/

JAVA NIO之文件通道相关推荐

  1. Java 7:在不丢失数据的情况下关闭NIO.2文件通道

    关闭异步文件通道可能非常困难. 如果您将I / O任务提交到异步通道,则需要确保正确执行了任务. 实际上,出于多种原因,这对于异步通道可能是一个棘手的要求. 默认的通道组使用守护进程线程作为工作线程, ...

  2. Java 7#8:测试台上的NIO.2文件通道

    关于新JDK 7功能的另一篇博客文章. 这次我正在写有关新的AnsynchronousFileChannel类的文章. 我将在两周内深入分析新的JDK 7功能,并决定连续编号我的帖子. 只是为了确保我 ...

  3. Java NIO之Channel(通道)

    **Java高级特性增强-NIO 本部分网络上有大量的资源可以参考,在这里做了部分整理并做了部分勘误,感谢前辈的付出,每节文章末尾有引用列表~ 写在所有文字的前面:作者在此特别推荐Google排名第一 ...

  4. java nio拷贝文件_Java 7 – NIO文件革命

    java nio拷贝文件 Java 7("项目代币")已于去年7月问世. 此版本中的新增功能很有用,例如,尝试资源-从try块中自动处理可关闭的资源,switch语句中的字符串,用 ...

  5. 实验六 : java nio 写文件速度

    java nio 写文件的速度与io 写文件速度相当, 例子3_3 package experiment3_3; import java.io.FileNotFoundException; impor ...

  6. Java NIO 读取文件、写入文件、读取写入混合

    前言 Java NIO(new/inputstream outputstream)使用通道.缓冲来操作流,所以要深刻理解这些概念,尤其是,缓冲中的数据结构(当前位置(position).限制(limi ...

  7. java nio MappedByteBuffer 文件映射

    MappedByteBuffer是java nio引入的文件内存映射方案,读写性能极高.NIO最主要的就是实现了对异步操作的支持.其中一种通过把一个套接字通道(SocketChannel) 注册到一个 ...

  8. 惊!一文看懂Java NIO读写文件

    Java NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式.很多小伙伴可能和我一样,对于习惯了 ...

  9. Java NIO 学习:通道(Channel)

    2019独角兽企业重金招聘Python工程师标准>>> 通道(Channel)用于在字节缓冲区(通道只能在字节缓冲区上操作)和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地 ...

最新文章

  1. EntLib 3.1学习笔记(6) : Security Application Block
  2. Hologres揭秘:如何支持超高QPS在线服务(点查)场景
  3. java 张量运算,博客 | Tensorflow_01_运算符与张量值
  4. iOS之页面布局-踩坑的原由
  5. 嵌入式操作系统内核原理和开发(事件)
  6. AutoCAD ObjectARX(VC)开发基础与实例教程2014版光盘镜像
  7. MemCache详细解读(转)
  8. linux系统同时安装python2.x和3.x
  9. 汉字转拼音,多音字解决方案
  10. scratch编程体感游戏
  11. 项目经理必须学会的财务知识
  12. c语言 int 溢出,C语言判断整数溢出
  13. python爬取58同城租房信息_分页爬取58同城租房信息.py
  14. Excel数据透视表: GetPivotData
  15. 神经科学界大地震!诺奖级泰斗将携团队移居中国,与蒲慕明院士强强联手
  16. 计算机中函数vlookup怎么用,教您使用excel函数vlookup
  17. python实现陷波滤波器、低通滤波器、高斯滤波器、巴特沃斯滤波器
  18. Linkage mapper 重大事故——文末问卷链接咨询
  19. 学业计算机水平考试试题,信息技术学业水平考试试题
  20. 制作网页过程中,经常用到的代码

热门文章

  1. 基于@AspectJ配置Spring AOP之一--转
  2. IOS审核的各个状态的时间
  3. 微信8年,干掉了短信也杀死了媒体?
  4. 2018香港纷智金融科技峰会 金色财经现场图文直播报道
  5. 开发者成功使用机器学习的十大诀窍
  6. java校园足球管理系统_基于jsp的校园足球管理平台-JavaEE实现校园足球管理平台 - java项目源码...
  7. Visual Studio 2013开发 mini-filter driver step by step 获取可执行文件名称 - 实现process monitor的一个功能 (10)
  8. Visual Studio 2013开发 mini-filter driver step by step 内核中使用线程(7)
  9. Redis进阶-JedisCluster初始化 自动管理连接池中的连接 _ 源码分析
  10. Spring Cloud【Finchley】- 20使用@RefreshScope实现配置的刷新