Java NIO是什么
J2SE 1.4引进的Java IO新特性,实现JSR 51,是Java原IO系统的增强和补充,故取New IO之名。相关的类都放在java.nio
包下。
为什么需要Java nio
两个字,效率,NIO能够处理的所有场景,原IO基本都能做到,NIO因效率而生,效率包括处理速度和吞吐量(Througthout, Scalability)。Java原IO都是流式的(Stream Oriented),一个Byte一个Byte的读取,且需要在JVM(用户空间)和操作系统内核空间之间复制数据(Bytes),速度较慢。IO主要分两块,文件系统IO和网络IO。文件系统方面,通过批量处理(Buffer),操作直接委托给操作系统(Direct),充分的利用操作系统的IO能力,提高访问性能,不过NIO还不支持文件系统的异步调用。网络IO方面,提供非阻塞操作,减少处理网络IO的线程数,增强可伸缩性(Scalability)。额外提一下,线程切换(Context Swith)是繁重的,多了会严重影响性能(可伸缩性, Scalability),线程越多越糟糕,n核的机器参考的线程数是n或n+1。
Java NIO的三个重要抽象
Java NIO引入的最重要三个抽象是Buffer,Channel,Selector,因为Java NIO中文件系统部分是阻塞的的,故不会用到Selector。下面分别来介绍。
Buffer
读或写数据的暂存容器,与Channel匹配使用。提供批量的向Channel写入或者读取数据功能。有四个int属性需要理解:
-
mark
标记的位置,default -1。 -
position
游标当前位置 -
limit
存活(可用)Byte最后一个的位置 -
capacity
Buffer的最大容量
很多Buffer的操作都与上面四个属性有关,比如flip()
,rewind()
, clear()
等。最重要的一个Buffer实现是ByteBuffer,主要实现对Byte的存取操作,常用的还有CharBuffer。
Channel
Client与IO设备之间读写的交互通道,数据先放到Buffer里面,再经Channel写入设备,或者从Channel里面读取数据。常用的有FileChannel,ServerSocketChannel和SocketChannel。
Selector
针对非阻塞(No-Blocking)的Channel,提供事件通知的机制。很多教程强调Selector提供了多路复用(单个线程或少数线程负责多个Channel的事件通知)的机制,作为Node.js的粉丝,我觉得Selector太复杂了,解释也很难懂。依我的理解,非阻塞式编程是基于事件的,Selector就是事件的通讯机制。非阻塞式才是关键,Event Bus构建非阻塞式编程的环境。
Java NIO不是终点
在一个理想的n核环境里,有n或者n+1个线程在拼命的工作(非阻塞式的在n个核上执行CPU指令),很少切换线程,充分利用CPU资源。但是现在JDBC依然是阻塞,虽然网络的交互非阻塞了,收到Message之后通常需要操作数据库,操作数据库的过程是阻塞的,需要大量的线程来等待。根据木桶原理,依然需要大量的线程,依然没能拥有完美的伸缩性(scalability)。比原来好多了,但还不是终点。有人争辩说,我用多线程服务器工作得很好,有多少多少的吞吐量云云,同学,可以更好的,不是吗?
文件操作的一个小例子
本来文件操作是非常简单的,无需多言,但ByteBuffer与字符(Char)之间还有一个编码解码的过程,要是ByteBuffer里面的编码结束之后还剩Byte怎么办?要留到下一次read一起编码。例子参考了Java官网的Example。注意rewind()
,flip()
。
public void readFileNio(String filePath) throws Exception { File file = new File(filePath); if (!(file.exists() && file.isFile())) { throw new Exception("file not exist"); } String encoding = System.getProperty("file.encoding"); FileInputStream fileInputStream = new FileInputStream(file); FileChannel channel = fileInputStream.getChannel(); System.out.println(channel.size()); ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024); while (channel.read(byteBuffer) > 0) { byteBuffer.rewind(); CharBuffer charBuffer = Charset.forName(encoding) .decode(byteBuffer); System.out.print(charBuffer.toString()); byteBuffer.flip(); } fileInputStream.close(); }