java中的nio包,对于java程序员来说是个熟悉又陌生的东西。以前一直以为nio=Non-blocking I/O,即非阻塞IO。后来又听人说nio其实是new IO新一代IO的意思。两种说法到底哪种是正确的?我去Oracle的java官网查看doc,很遗憾也没直接解释nio是什么单词的缩写。但是经过一番实践,确证new IO才是nio正确的全称。
一段代码引发的思考
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channelA.read(buf);
buf.flip();
while(buf.hasRemaining()) {
channelB.write(buf);
}
这是在网上搜FileChannel看到的一段代码。这个时候我以为nio下的一切io操作都是非阻塞的,于是看到这段代码我就非常费解。
为什么FileChannel在read的时候没有加while判断,在write的时候就要加while判断了?难道说对FileChannel来说read是线程阻塞操作,write是非阻塞的?这就有点匪夷所思了。看到doc文档也没找到答案。
于是我自己试验了一下,在while循环里添加日志,如果write是非阻塞的,那么while应该打印很多次。最后的结果是while里只打印一次日志,代表read和write都是阻塞的!
这时候其实我更迷惑了,因为我一直以为nio下的io操作都是非阻塞的。
于是再次去百度FileChannel到底是阻塞还是非阻塞的。找了很久没找到确切的答案,最后在StackOverflow上找到了一个比较满意的回答:
UNIX does not support non-blocking I/O for files, see Non-blocking I/O with regular files. As Java should (at least try to) provide the same behaviour on all platforms, the FileChannel does not implement SelectableChannel.
UNIX不支持文件的非阻塞IO,参看这个网页的说明。由于Java在全平台上要保持行为一致(或者努力这么做),所以FileChannel没有实现SelectableChannel。
However Java 7 will include a new AsynchronousFileChannel class that supports asynchronous file I/O, which is a different mechanism to non-blocking I/O.
但是Java7上引入了一个支持异步文件IO的AsynchronousFileChannel类,但是这是跟非阻塞IO不一样的机制。
In general only sockets and pipes truly support non-blocking I/O via select() mechanism.
一般来说只有socktes和pipes通过select机制真正支持非阻塞IO。
从这段话来说,首先FileChannel肯定是阻塞IO的;其次实现了SelectableChannel接口才能实现非阻塞IO。从FileChannel上也能看出来,nio下不是所有io操作都是非阻塞的。因此nio的全称绝不应该是non-blocking I/O,而应该是new IO。
当然这里还说了异步和非阻塞是完全不一样的机制,这个以后再来了解吧。
Channels&Buffers
nio下有很多类,对我们来说以下三个是最重要的:
- Channels
- Buffers
- Selectors
第一代io使用字节流和字符流来进行读写,而nio使用Channels和Buffers来实现读写。数据总是从Channel读到buffer里,再从buffer写到Channel里。这个是使用上很大的区别。
Channel和字符字节流有点类似,但是Channel都是双向的,既可以读,也可以写。
以前的io流我们一般直接定义一个byte数组来作为buffer,但是nio里需要使用Buffer类。
下面是网上找的一个使用FileChannel和Buffer的例子:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//创建48个字节长度的ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //从Channel中读取数据到buffer里
while (bytesRead != -1) {
buf.flip(); //切换buffer到读取状态
while(buf.hasRemaining()){
System.out.print((char) buf.get()); //每次读取一个字节
}
buf.clear(); //清空buffer后才能继续从Channel里读取数据
bytesRead = inChannel.read(buf);
aFile.close();
Buffer实例化的时候需要传入长度。在buffer写好数据,准备读取到Channel里的时候,一定要执行flip操作,buffer才可以读取。同样的,再次写入数据之前,buffer需要clear一下清除数据。
Selector和非阻塞IO
但是nio最吸引我们的肯定还是非阻塞IO,而java nio里非阻塞IO的实现要靠Selector。Selector是一种IO多路复用机制的实现。简单来说,就是用单个线程/进程来对多个IO Channel进行轮询。内核不管IO有没有完成都会立即返回给用户,这样单个IO的阻塞就不会阻塞用户线程。单个线程无需一个个等待IO完成再工作,而是发现有完成的就处理,处理完继续轮询,直到下一个完成的IO出现。
这样做的好处就是单个线程即可处理海量并发请求,当然前提是业务的数据处理不能太耗时,否则线程会卡在数据处理上。NodeJs就是单线程非阻塞IO模型,因此擅长处理高并发。同样适合非阻塞IO的还有nginx、redis等高并发但是计算简单的软件。而Java数据库IO连接的JDBC是阻塞io,因此Java服务器不太适合nio,而适合多线程来处理并发。
对Android程序员来说,Looper中的MessgeQueue在执行next方法获取下一条handler消息的时候,如果没有消息,主线程执行nativePollOnce方法阻塞住。这样主线程在没事的时候就不会消耗cpu资源。在有消息传过来时,android framework除了把消息添加到MessageQueue,还会把主线程给wakeup。那么怎么样block(阻塞)线程和wakeup(唤醒)线程呢?通过linux系统的epoll机制,而epoll机制就是selector相对的poll机制的加强版。
以上只是我的一点形而上的理解,有可能存在谬误,推荐几个解释的好的博文来学习Selector以及IO多路复用的概念: