背景知识
同步、异步、阻塞、非阻塞
首先,这几个概念非常容易搞混淆,但nio中又有涉及,所以总结一下。
同步:api调用返回时调用者就知道操作的结果如何了(实际读取/写入了多少字节)。
异步:相对于同步,api调用返回时调用者不知道操作的结果,后面才会回调通知结果。
阻塞:当无数据可读,或者不能写入所有数据时,挂起当前线程等待。
非阻塞:读取时,可以读多少数据就读多少然后返回,写入时,可以写入多少数据就写入多少然后返回。
对于i/o操作,根据oracle官网的文档,同步异步的划分标准是“调用者是否需要等待i/o操作完成”,这个“等待i/o操作完成”的意思不是指一定要读取到数据或者说写入所有数据,而是指真正进行i/o操作时,比如数据在tcp/ip协议栈缓冲区和jvm缓冲区之间传输的这段时间,调用者是否要等待。
所以,我们常用的read()和write()方法都是同步i/o,同步i/o又分为阻塞和非阻塞两种模式,如果是非阻塞模式,检测到无数据可读时,直接就返回了,并没有真正执行i/o操作。
总结就是,java中实际上只有同步阻塞i/o、同步非阻塞i/o与异步i/o三种机制,我们下文所说的是前两种,jdk1.7才开始引入异步i/o,那称之为nio.2。
传统io
我们知道,一个新技术的出现总是伴随着改进和提升,javanio的出现亦如此。
传统i/o是阻塞式i/o,主要问题是系统资源的浪费。比如我们为了读取一个tcp连接的数据,调用inputstream的read()方法,这会使当前线程被挂起,直到有数据到达才被唤醒,那该线程在数据到达这段时间内,占用着内存资源(存储线程栈)却无所作为,也就是俗话说的占着茅坑不拉屎,为了读取其他连接的数据,我们不得不启动另外的线程。在并发连接数量不多的时候,这可能没什么问题,然而当连接数量达到一定规模,内存资源会被大量线程消耗殆尽。另一方面,线程切换需要更改处理器的状态,比如程序计数器、寄存器的值,因此非常频繁的在大量线程之间切换,同样是一种资源浪费。
随着技术的发展,现代操作系统提供了新的i/o机制,可以避免这种资源浪费。基于此,诞生了javanio,nio的代表性特征就是非阻塞i/o。紧接着我们发现,简单的使用非阻塞i/o并不能解决问题,因为在非阻塞模式下,read()方法在没有读取到数据时就会立即返回,不知道数据何时到达的我们,只能不停的调用read()方法进行重试,这显然太浪费cpu资源了,从下文可以知道,selector组件正是为解决此问题而生。
1.channel
概念
javanio中的所有i/o操作都基于channel对象,就像流操作都要基于stream对象一样,因此很有必要先了解channel是什么。以下内容摘自jdk1.8的文档
achannelrepresentsanopenconnectiontoanentitysuchasahardwaredevice,afile,anetworksocket,oraprogramcomponentthatiscapableofperformingoneormoredistincti/ooperations,forexamplereadingorwriting.
从上述内容可知,一个channel(通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道是javanio提供的一座桥梁,用于我们的程序和操作系统底层i/o服务进行交互。
通道是一种很基本很抽象的描述,和不同的i/o服务交互,执行不同的i/o操作,实现不一样,因此具体的有filechannel、socketchannel等。
通道使用起来跟stream比较像,可以读取数据到buffer中,也可以把buffer中的数据写入通道。
当然,也有区别,主要体现在如下两点:
一个通道,既可以读又可以写,而一个stream是单向的(所以分inputstream和outputstream)
通道有非阻塞i/o模式
实现
javanio中最常用的通道实现是如下几个,可以看出跟传统的i/o操作类是一一对应的。
filechannel:读写文件
datagramchannel:udp协议网络通信
socketchannel:tcp协议网络通信
serversocketchannel:监听tcp连接
2.buffer
nio中所使用的缓冲区不是一个简单的byte数组,而是封装过的buffer类,通过它提供的api,我们可以灵活的操纵数据,下面细细道来。
与java基本类型相对应,nio提供了多种buffer类型,如bytebuffer、charbuffer、intbuffer等,区别就是读写缓冲区时的单位长度不一样(以对应类型的变量为单位进行读写)。
buffer中有3个很重要的变量,它们是理解buffer工作机制的关键,分别是
capacity(总容量)
position(指针当前位置)
limit(读/写边界位置)
buffer的工作方式跟c语言里的字符数组非常的像,类比一下,capacity就是数组的总长度,position就是我们读/写字符的下标变量,limit就是结束符的位置。buffer初始时3个变量的情况如下图
在对buffer进行读/写的过程中,position会往后移动,而limit就是position移动的边界。由此不难想象,在对buffer进行写入操作时,limit应当设置为capacity的大小,而对buffer进行读取操作时,limit应当设置为数据的实际结束位置。(注意:将buffer数据写入通道是buffer读取操作,从通道读取数据到buffer是buffer写入操作)
在对buffer进行读/写操作前,我们可以调用buffer类提供的一些辅助方法来正确设置position和limit的值,主要有如下几个
flip():设置limit为position的值,然后position置为0。对buffer进行读取操作前调用。
rewind():仅仅将position置0。一般是在重新读取buffer数据前调用,比如要读取同一个buffer的数据写入多个通道时会用到。
clear():回到初始状态,即limit等于capacity,position置0。重新对buffer进行写入操作前调用。
compact():将未读取完的数据(position与limit之间的数据)移动到缓冲区开头,并将position设置为这段数据末尾的下一个位置。其实就等价于重新向缓冲区中写入了这么一段数据。
然后,看一个实例,使用filechannel读写文本文件,通过这个例子验证通道可读可写的特性以及buffer的基本用法(注意filechannel不能设置为非阻塞模式)。