NIO全称New IO。从J2SE 1.4开始NIO一直断断续续的有新的功能加入。
总结起来主要有以下内容:
DirectByteBuffer
Channel
非阻塞IO - Non Blocking IO
选择器
管道Pipe
异步IO Asynchronous IO
文件操作API
DirectByteBuffer
在传统IO方式下,通常我们在Java中通过一个字节数组作为缓冲区来作为读写方法的参数。但是此种方式比较低效:首先,因为它收垃圾回收器管理,在本地程序中的内存地址是变化的,无法交给作为操作系统调用的参数;因此,本地代码调用操作系统IO API时需要另辟缓冲区,每次读写都要在本地代码缓冲区和字节数组直接复制数据。
比如从下面io_util.c可以看到先使用IO_read把内容读到C语言申请的buf中,之后使用SetByteArrayRegion虚拟机API来复制内容到bytes这个Java字节数组中。
NIO中引入了ByteBuffer。ByteBuffer有两种实现方式,一种是HeapBuffer,就是包了个byte[]。另外一种是DirectBuffer,由本地代码申请,存在于Java堆之外。DirectBuffer的存在为Java语言提供了引用堆外内存的能力,避免了重复的byte[], 在不同的Channel之间复制数据也更加高效。但请注意DirectBuffer在堆外,因此不受Java -Xmx的控制。
DirectByteBuffer使用UNSAFE来申请内存:
而Unsafe是通过malloc来申请内存。
另外NIO提供了对应于各种原始类型的Buffer(Boolean除外),比如IntBuffer, ShortBuffer等。在ByteBuffer上可以创造出其它类型的Buffer视图。
Channel
NIO中引入了Channel来代替传统的InputStream/OutputStream和Socket等。Channel也提供read/write等方法,并且以ByteBuffer作为参数。
Channel接口本身只定义了isOpen和close两个方法。但是Channel有几个主要的子接口:
ReadableByteChannel - 可读通道
WritableByteChannel - 可写通道
SelectableChannel - 可筛选通道,后面讲的Selector会用到
AsynchronousChannel - 异步通道
NetworkChannel - 网络通道
InterrruptableChannel - 可打断通道
而实现方面,其实最要要的是网络和文件两方面的,毕竟IO也主要就是文件和网络两件事情。以下是一些常用的抽象类:
FileChannel - 文件通道,可以读写文件。
ServerSocketChannel - 服务器套接字通道,开启网络端口接受请求。
SocketChannel - 套接字通道,作为网络客户端时使用。
DatagramChannel - 数据报的通道,UDP通信用。
这些Channel可以单独使用,同时传统IO API也加入了各种getChannel的方法。比如,可以直接使用FileChannel#open,也可以从FileInputStream#getChannel获取FileChannel。
非阻塞Non Blocking IO
传统的IO方式下InputStream/OutputStream读写时线程会被阻塞。也就是说调用write方法会等到内容写出去才能返回;调用read方法一般至少读到1个字符才能返回。
在非阻塞模式下可以询问是否有新的网络连接进来或者是否有可以读写数据;IO操作在非阻塞模式下立刻返回,也即可以返回空值。这样就让一个线程处理很多IO通道成为可能,减少了线程的数量,因而节约每个线程必须占有的内存和线程调度开销。
支持这些非阻塞IO的类都继承自SelectableChannel。它有configureBlocking(boolean)方法。
注意:非阻塞IO只支持网络IO。不支持文件IO。
在实现方面,主流操作系统都对网络IO有非阻塞支持。因为网络IO本身的低速和不确定性,这也是操作系统必备的功能。相反,由于文件操作相对较快较可靠,很多操作系统对文件IO并没有非阻塞方式的支持。
选择器 Selector
选择器是结合阻塞和非阻塞模式的各自优点而存在。
首先,Selector用来处理很多非阻塞IO通道。很多个非阻塞模式的通道可以注册在一个Selector上。然后有一个线程来通过Selector来操作很多IO通道。
另外,Selector#select操作是阻塞的。因此处理线程无须不停的查询是否有任务可以做,这样做减少CPU空转。在有通道就绪时,select方法会返回,程序可以查询selectedKeys的到有哪些通道可以操作了。另外Selector提供了wakeUp方法来主动唤醒阻塞在上边的工作线程。
比如下面的例子说明了如何使用Selector和NIO的Channel:
在Selector的实现原理方面,Windows Sockets 2里边有select函数。Windows 实现的Selector可能需要一些帮助线程并发调用select,因为select API限制每次查询的fd个数。因此Windows此处的实现稍微复杂。另外为了实现wakeUp功能WindowsSelectorImpl使用了Pipe,在select时除了多个IO通道的FD外还包含了Pipe的source FD,因此在Pipe sink端给个信号它就能醒过来。关于Pipe和Windows下Pipe的实现接下来会说到。
Linux则是通过其epoll机制实现。wakeUp同样是通过其Pipe实现。
管道 Pipe
管道在Unix中由来已久。大家都知道多个命令通过管道操作|可以协同工作完成强大的功能。NIO的管道与此类似,有一个输入端和一个输出端分别叫做SinkChannel和SourceChannel。这两个Channel都是Selectable的,因此可以用于Selector。
但是很明显NIO的Pipe并不是用来做进程间通信的,而是支持线程间的NIO方式通信的。比如上面提到的Selector的wakeUp就是通过Pipe实现的。当一个线程阻塞在Selector#select上时,另外一个线程可以通过Pipe通知它从阻塞状态返回。
注意:Pipe并不是用来做普通的线程间通信的。普通的线程间通信通过共享的进程内存和相应的锁机制就可以实现,比如用BlockingQueue。
在Pipe实现方面,Windows下的PipeImpl有点奇葩。它通过一个自己到自己的TCP连接实现。先打开一个本地loopback(localhost) 的服务端口,然后自己连上去,这样就有了一对互相通信的Socket。这么做大概是因为Windows本身的Pipe API不是十分好用,很多人在其它场合也倾向使用Socket来代替Windows下的Pipe API。
在Linux/Unix下Pipe有悠久的历史和非常成熟的支持,因此Linux下Pipe的创建只需要一个pipe函数就完成了。这一局Linux/Unix完胜。
异步IO - AIO - Asynchronous IO
Asynchronous IO也可以不阻塞调用进程的方式做读写操作。在IO操作时可以通过指定一个CompletionHandler在IO完成后回调相应的逻辑代码;或者返回一个Future对象供查询完成状态和获取结果。因此,虽然文件IO不支持Non Blocking和Selector方式,但是可以通过AsynchronousFileChannel来实现异步操作:
在实现方面,Linux/Unix使用的是SimpleAsynchronousFileChannelImpl。它其实是通过维护一个线程池来做阻塞式IO的。
Windows对AIO有原生支持。并不需要阻塞方式的IO。虽然为了运行CompletionHandler还是需要一个线程池,但是能比Linux少用一些线程。
Windows这次扳回一局,
文件操作API
NIO加入了更加丰富的文件操作API。比如Files可以更方便的处理文件读写、遍历目录树等。另外NIO提供了对文件元数据、存取权限(ACL)、文件链接的支持等。这部分相对比较容易理解,只是对以前缺少的文件API做了补充。在此不做赘述。
领取专属 10元无门槛券
私享最新 技术干货