黑马程序员技术交流社区

标题: 【上海校区】Java NIO(五)——NIO总结及NIO新特性 [打印本页]

作者: 不二晨    时间: 2018-7-16 11:02
标题: 【上海校区】Java NIO(五)——NIO总结及NIO新特性

本文转自:https://blog.csdn.net/a953713428/article/details/64907250

我们知道是NIO是在2002年引入到J2SE 1.4里的,很多Java开发者比如我还是不知道怎么充分利用NIO,更少的人知道在Java SE 7里引入了更新的输入/输出 API(NIO.2)。但是对于普通的开发者来说基本的I/O操作就够用了,而NIO则是在处理I/O性能优化方面带来显著性效果。更快的速度则意味着NIO和NIO.2的API暴露了更多低层次的系统操作的入口,这对于开发者而言则意味着更复杂的操作和精巧的程序设计。从前面的几节的讲解来看NIO的操作无不繁琐。要完全掌握还是有点难度的。前面我们讲解了Buffer,Channel,Selector,都是从大的面上去探讨NIO的主要组件。这一节我们则从NIO的特性方面去探讨更细节的一些问题。

1.NIO的新特性

总的来说java 中的IO 和NIO的区别主要有3点:

NIO在基础的IO流上发展处新的特点,分别是:内存映射技术,字符及编码,非阻塞I/O和文件锁定。下面我们分别就这些技术做一些说明。

2. 内存映射

这个功能主要是为了提高大文件的读写速度而设计的。内存映射文件(memory-mappedfile)能让你创建和修改那些大到无法读入内存的文件。有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问了。将文件的一段区域映射到内存中,比传统的文件处理速度要快很多。内存映射文件它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。

NIO中内存映射主要用到以下两个类:

下面我们通过一个例子来看一下内存映射读取文件和普通的IO流读取一个150M大文件的速度对比:

实验结果为:

效果对比还是挺明显的。我们看到在上面程序中调用FileChannel类的map方法进行内存映射,第一个参数设置映射模式,现在支持3种模式:

我们注意到FileChannel类中有map方法来建立内存映射,按理说是否应用的有相应的unmap方法来卸载映射内存呢。但是竟然没有找到该方法。一旦建立映射保持有效,直到MappedByteBuffer对象被垃圾收集。 此外,映射缓冲区不会绑定到创建它们的通道。 关闭相关的FileChannel不会破坏映射; 只有缓冲对象本身的处理打破了映射。

内存映射文件的优点:

下面我们再写一个复制文件的例子来看一下对于一个120M的文件通过这种方式到底能有多快速度的提升:

结果为:

这个速度还是相当惊人的!

2. 字符及编码

说到字符和编码,我们的先说一个概念,字符编码方案:

编码方案定义了如何把字符编码的序列表达为字节序列。字符编码的数值不需要与编码字节相同,也不需要是一对一或一对多个的关系。原则上,把字符集编码和解码近似视为对象的序列化和反序列化。

通常字符数据编码是用于网络传输或文件存储。编码方案不是字符集,它是映射;但是因为它们之间的紧密联系,大部分编码都与一个独立的字符集相关联。例如,UTF-8,仅用来编码Unicode字符集。尽管如此,用一个编码方案处理多个字符集还是可能发生的。例如,EUC可以对几个亚洲语言的字符进行编码。

目前字符编码方案有US-ASCII,UTF-8,GB2312, BIG5,GBK,GB18030,UTF-16BE, UTF-16LE, UTF-16,UNICODE。其中Unicode试图把全世界所有语言的字符集统一到全面的映射之中。虽然战友一定的市场份额,但是目前其余的字符方案仍然广被采用。大部分的操作系统在I/O与文件存储方面仍是以字节为导向的,所以无论使用何种编码,Unicode或其他编码,在字节序列和字符集编码之间仍需要进行转化。

由java.nio.charset包组成的类满足了这个需求。这不是Java平台第一次处理字符集编码,但是它是最系统、最全面、以及最灵活的解决方式。
下面我们通过一个小例子来看一下通过不同的Charset实现如何把字符翻译成字节序列:

输出为:

2.1 字符集编码器和解码器

字符的编码和解码是使用很频繁的,试想如果使用UTF-8字符集进行编码,但是却是用UTF-16字符集进行解码,那么这条信息对于用户来说其实是无用的。因为没人能看得懂。在NIO中提供了两个类CharsetEncoder和CharsetDecoder来实现编码转换方案。

CharsetEncoder类是一个状态编码引擎。实际上,编码器有状态意味着它们不是线程安全的:CharsetEncoder对象不应该在线程中共享。CharsetEncoder对象是一个状态转换引擎:字符进去,字节出来。一些编码器的调用可能需要完成转换。编码器存储在调用之间转换的状态。

字符集解码器是编码器的逆转。通过特殊的编码方案把字节编码转化成16-位Unicode字符的序列。与CharsetEncoder类似的, CharsetDecoder也是状态转换引擎。

3. 非阻塞IO

一般来说 I/O 模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞 四种IO模型。

同步阻塞 IO :

在此种方式下,用户进程在发起一个 IO 操作以后,必须等待 IO 操作的完成,只有当真正完成了 IO 操作以后,用户进程才能运行。 JAVA传统的 IO 模型属于此种方式!

同步非阻塞 IO:

在此种方式下,用户进程发起一个 IO 操作以后可以返回做其它事情,但是用户进程需要时不时的询问 IO 操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的 CPU 资源浪费。其中目前 JAVA 的 NIO 就属于同步非阻塞 IO 。

异步阻塞 IO :

此种方式下是指应用发起一个 IO 操作以后,不等待内核 IO 操作的完成,等内核完成 IO 操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问 IO 是否完成,那么为什么说是阻塞的呢?因为此时是通过 select 系统调用来完成的,而 select 函数本身的实现方式是阻塞的,而采用 select 函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性!

异步非阻塞 IO:

在此种模式下,用户进程只需要发起一个 IO 操作然后立即返回,等 IO 操作真正的完成以后,应用程序会得到 IO 操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的 IO 读写操作,因为 真正的 IO读取或者写入操作已经由 内核完成了。目前 Java 中还没有支持此种 IO 模型。

上面我们说到nio是使用了同步非阻塞模型。我们知道典型的非阻塞IO模型一般如下:

但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。所以这就不得不说到下面这个概念–多路复用IO模型。

多路复用IO模型

在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。

NIO 的非阻塞 I/O 机制是围绕 选择器和 通道构建的。 Channel 类表示服务器和客户机之间的一种通信机制。Selector 类是 Channel 的多路复用器。 Selector 类将传入客户机请求多路分用并将它们分派到各自的请求处理程序。NIO 设计背后的基石是反应器(Reactor)设计模式。

关于Reactor模式在此就不多做介绍,网上很多。Reactor负责IO事件的响应,一旦有事件发生,便广播发送给相应的handler去处理。而NIO的设计则是完全按照Reactor模式来设计的。Selector发现某个channel有数据时,会通过SelectorKey来告知,然后实现事件和handler的绑定。

在Reactor模式中,包含如下角色:

我们简单写一个利用了Reactor模式的NIO服务端:

这种方式带来的好处也是不言而喻的。利用多路复用机制避免了线程的阻塞,提高了连接的数量。一个线程就可以管理多个socket,只有当socket真正有读写事件发生才会占用资源来进行实际的读写操作。虽然多线程+ 阻塞IO 达到类似的效果,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。

另外多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。

4. 文件锁定

NIO中的文件通道(FileChannel)在读写数据的时候主 要使用了阻塞模式,它不能支持非阻塞模式的读写,而且FileChannel的对象是不能够直接实例化的, 他的实例只能通过getChannel()从一个打开的文件对象上边读取(RandomAccessFile、 FileInputStream、FileOutputStream),并且通过调用getChannel()方法返回一个 Channel对象去连接同一个文件,也就是针对同一个文件进行读写操作。

文件锁的出现解决了很多Java应用程序和非Java程序之间共享文件数据的问题,在以前的JDK版本中,没有文件锁机制使得Java应用程序和其他非Java进程程序之间不能够针对同一个文件共享 数据,有可能造成很多问题,JDK1.4里面有了FileChannel,它的锁机制使得文件能够针对很多非 Java应用程序以及其他Java应用程序可见。但是Java里面 的文件锁机制主要是基于共 享锁模型,在不支持共享锁模型的操作系统上,文件锁本身也起不了作用,JDK1.4使用文件通道读写方式可以向一些文件 发送锁请求,
FileChannel的 锁模型主要针对的是每一个文件,并不是每一个线程和每一个读写通道,也就是以文件为中心进行共享以及独占,也就是文件锁本身并不适合于同一个JVM的不同 线程之间。

我们简要看一下相关API:

锁定区域的范围不一定要限制在文件的size值以内,锁可以扩展从而超出文件尾。因此,我们可以提前把待写入数据的区域锁定,我们也可以锁定一个不包含任何文件内容的区域,比如文件最后一个字节以外的区域。如果之后文件增长到达那块区域,那么你的文件锁就可以保护该区域的文件内容了。相反地,如果你锁定了文件的某一块区域,然后文件增长超出了那块区域,那么新增加 的文件内容将不会受到您的文件锁的保护。

我们写一个简单实例:

上面我们总结了NIO的4个新特性,对于IO来说都是很重要的功能以及性能的升级。下面我们写一个完整的NIO Socket客户端和服务端,总结一下NIO 的用法,每一行都加了注释:

服务端:

客户端:







作者: 不二晨    时间: 2018-7-16 11:43
奈斯
作者: 吴琼老师    时间: 2018-7-18 14:25

作者: 摩西摩西OvO    时间: 2018-7-19 14:27





欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2