Java的BIO,NIO和AIO的区别于演进
作者:公众号:
前言
java里面的io模型种类较多,主要包括bio,nio和aio,每个io模型都有不一样的地方,那么这些io模型是如何演变呢,底层的原理又是怎样的呢? 本文我们就来聊聊。
bio
bio全称是blocking io,是jdk1.4之前的传统io模型,本身是同步阻塞模式,针对网络通信都是一请求一应答的方式,虽然简化了上层的应用开发,但在性能和可靠性方面存在着巨大瓶颈,试想一下如果每个请求都需要新建一个线程来专门处理,那么在高并发的场景下,机器资源很快就会被耗尽,当然,我们可以通过线程池来优化这种情况,但即使是这样,仍然改变不了阻塞io的根本问题,就是在io执行的两个阶段都被block了。拿一个read操作来举例子,在linux中,应用程序向linux发起read操作,会经历两个步骤:
第一个阶段linux内核首先会把需要读取的数据加载到操作系统内核的缓冲区中(linux文件系统是缓存io,也称标准io)
第二个阶段应用程序拷贝内核里面的数据到自己的用户空间中
如果是socket操作,类似也会经历两个步骤:
第一个阶段:通常涉及等待网络上的数据分组包到达,然后被复制到内核的缓冲区
第二个阶段:把数据从内核缓冲区,从内核缓冲区拷贝到用户进程的内存空间里面
同步阻塞io之所以效率低下,就是因为在这两个阶段,用户的线程或者进程都是阻塞的,期间虽然不占cpu资源,但也意味着该线程也不能再干其他事。有点站着茅坑不拉屎的感觉,自己暂时不用了,也不让别人用。
图示如下:
nio
由于bio的缺点,导致java在jdk1.0至jdk3.0中,网络通信模块的性能一直是短板,所以很多人更倾向于使用c/c++开发高性能服务端。为了强化java在服务端的市场,终于在jsr-51也就是jdk4.0的时候发布了java nio,可以支持非阻塞io。并新增了java.nio的包,提供很多异步开发的api和类库。
主要的类和接口如下:
(1)进行异步io操作的缓冲区bytebuffer
(2)进行异步io操作的管道pipe
(3)进行各种io操作的channel,主要包括serversocketchannel和socketchannel
(4)实现非阻塞io的多路复用器selector
nio主要有buffer、channel、selector三种技术的整合,通过零拷贝的buffer取得数据,每一个客户端通过channel在selector(多路复用器)上进行注册。服务端不断轮询channel来获取客户端的信息。channel上有connect,accept(阻塞)、read(可读)、write(可写)四种状态标识。根据标识来进行后续操作。所以一个服务端可接收无限多的channel。不需要新开一个线程。大大提升了性能。
新的nio类库,促进了异步非阻塞编程的发展和应用,但仍然有一些不足之处:
(1)没有统一的文件属性,例如读写权限
(2)api能力比较弱,例如目录的及联创建和递归遍历,往往需要自己完成。
(3)底层操作系统的一些高级api无法使用
(4)所有的文件操作都是同步阻塞调用,在操作系统层面上并不是异步文件读写操作。
java里面的nio其实采用了多路复用的io模式,多路复用的模式在linux底层其实是采用了select,poll,epoll的机制,这种机制可以用单个线程同时监听多个io端口,当其中任何一个socket的数据准备好了,就能返回通知用户线程进行读取操作,与阻塞io阻塞的是每一个用户的线程不一样的地方是,多路复用只需要阻塞一个用户线程即可,这个用户线程通常我们叫它selector,其实底层调用的是内核的select,这里面只要任何一个io操作就绪,就可以唤醒select,然后交由用户线程处理。用户线程读取数据这个过程仍然是阻塞的,多路复用技术只是在第一个阶段可以变为非阻塞调用,但在第二个阶段拷贝数据到用户空间,其实还是阻塞的,多路复用技术的最大特点是使用一个线程就可以处理很多的socket连接,尽管性能上不一定提升,但支持并发能力却大大增强了。
图示如下:
aio
aio,其实是nio的改进优化,也被称为nio2.0,在2011年7月,也就是jdk7的版本中发布,它主要提供了三个方面的改进:
(1)提供了能够批量获取文件属性的api,通过spi服务,使得这些api具有平台无关性。
(2)提供了aio的功能,支持基于文件的异步io操作和网络套接字的异步操作
(3)完成了jsr-51定义的通道功能等。
aio 通过调用accept方法,一个会话接入之后再次调用(递归)accept方法,监听下一次会话,读取也不再阻塞,回调complete方法异步进行。不再需要selector 使用channel线程组来接收。
从nio上面我们能看到,对于io的两个阶段的阻塞,只是对于第一个阶段有所改善,对于第二个阶段在nio里面仍然是阻塞的。而真正的理想的异步非阻塞io(aaio)要做的就是,将io操作的两个阶段都全部交给内核系统完成,用户线程只需要告诉内核,我要读取一块数据,请你帮我读取,读取完了放在我给你的地址里面,然后告诉我一声就可以了。
aio可以做到真正的异步的操作,但实现起来比较复杂,支持纯异步io的操作系统非常少,目前也就windows是iocp技术实现了,而在linux上,目前有很多开源的异步io库,例如libevent、libev、libuv,但基本都不是纯的异步io操作,底层还是是使用的epoll实现的。
图示如下:
nio与netty
既然java拥有了各种io体系,那么为什么还会出现netty这种框架呢?
netty出现的主要原因,如下:
(1)java nio类库和api繁杂众多,使用麻烦。
(2)java nio封装程度并不高,常常需要配合java多线程编程来使用,这是因为nio编程涉及到reactor模式。
(3)java nio异常体系不完善,如客户端面临断连,重连,网络闪断,半包读写,网络阻塞,异常码流等问题,虽然开发相对容易,但是可靠性和稳定性并不高。
(4)java nio本身的bug,修复较慢。
注意,真正的异步非阻塞io,是需要操作系统层面支持的,在windows上通过iocp实现了真正的异步io,所以java的aio的异步在windows平台才算真正得到了支持,而在linux系统中,仍然用的是epoll模式,所以在linux层面上的aio,并不是真正的或者纯的异步io,这也是netty里面为什么采用java的nio实现的,而并非是aio,主要原因如下:
(1)aio在linux上底层实现仍使用epoll,与nio相同,因此在性能上没有明显的优势
(2)windows的aio底层实现良好,但netty的开发者并没有把windows作为主要使用平台,所以优化考虑linux
总结
本文主要介绍了java里面io模型的演变和发展,这也是java在服务端领域大放异彩的一个重要原因,了解这些知识之后,我们再去学习高性能的netty框架,将会更加容易。