本篇文章目的在于基本概念和原理的解释,不会贴过多的使用代码。html
Java NIO (New IO)是 Java 的另外一个 IO API (来自 java1.4) ,意味着能够替代标准的 Java IO API和 Java Networking API。 提供了一种与标准 IO API 不一样的 IO 工做方式。
注意:Java的NIO只是说IO API,阻塞非阻塞才是IO的模型。java
也有人称NIO为No-Blocking IO,非阻塞IO,可是这么说并不严谨。由于对于基础的IO操做API(好比文件IO,FileChannel),仍是阻塞的模型。只有对Networking IO API才可使用非阻塞的模型(configureBlocking(false)
)。redis
Java NIO中的Networking IO API,支持非阻塞IO模型,还实现了IO多路复用(IO Multiplexing)。对于服务端来讲,能够用更少的线程支持更多的并发,大幅度提高了性能。编程
阻塞与非阻塞是从线程的角度出发的,这里指的是线程状态。服务器
当进行IO读写时,线程是阻塞的状态。此时会让出cpu控制权,不会占用cpu资源。网络
什么?不占用CPU资源?那是否是表明阻塞模型更好呢?多线程
答案是并非,虽然阻塞状态不会占用CPU,可是会发生线程的切换,线程切换时会有上下文保存转换的过程,须要CPU调度,是一个很昂贵的操做。并发
Java NIO中的基础IO API(非Networking IO API)仍是阻塞的方式,只是使用方式从面向流(stream)编程面向块(buffer)了,和BIO本质上并无什么区别。异步
非阻塞是指在进行IO操做的时候,若是设备还未准备好(好比socket尚未收到数据),操做会直接返回结果,不会让当前线程进入阻塞状态。socket
这样的优势是,使用者能够自行决定在数据未准备好时的操做。线程能够在没有数据期间去执行其余操做。
Networking API能够配置为非阻塞模型Channel.configureBlocking(false)
,配合Selector来实现多路复用功能。简单的说就是一个Selector监听多个socket io(对于unix系统来讲,socket也是一个fd,也属于io),能够在一个线程中支持多个链接。固然在实际服务器开发时,就算是NIO模型,有些程序也不会只使用一个线程;但相比传统的Blocking IO方式来讲,须要的线程数量也会大大减小了。(redis中就是使用了IO多路复用技术,而且只有一个线程监听socket io)
AIO 是 Java 1.7 以后引入的包,是 NIO 的升级版本,新增了提异步非阻塞的 IO 操做方式,因此人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操做以后会直接返回,不会堵塞在那里,当后台处理完成,操做系统会执行回调通知相应的线程进行后续的操做。
在I/O编程过程当中,当须要同时处理多个客户端请求时,能够利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术经过把多个I/O的阻塞复用到同一个Select的阻塞上,从而使得系统在单线程的状况下能够同时处理多个客户端请求。与传统的多线程/多进程模型相比,I/O多路复用的最大优点是系统开销小,系统不须要建立新的额外进程或者线程,也不须要维护这些线程和进程的运行,下降了系统的维护工做量,节省了系统的资源,I/O多路复用的主要应用场景以下:
目前支持I/O多路复用的系统调用又select/pselect/poll/epoll。
select/epoll的介绍摘自https://zhuanlan.zhihu.com/p/...
select的实现思路很直接。假如程序同时监视以下图的sock一、sock2和sock3三个socket,那么在调用select以后,操做系统把进程A分别加入这三个socket的等待队列中。
当任何一个socket收到数据后,中断程序将唤起进程。下图展现了sock2接收到了数据的处理流程。
所谓唤起进程,就是将进程从全部的等待队列中移除,加入到工做队列里面。以下图所示。
经由这些步骤,当进程A被唤醒后,它知道至少有一个socket接收了数据。程序只需遍历一遍socket列表,就能够获得就绪的socket。
这种简单方式行之有效,在几乎全部操做系统都有对应的实现。
可是简单的方法每每有缺点,主要是:
其一,每次调用select都须要将进程加入到全部监视socket的等待队列,每次唤醒都须要从每一个队列中移除。这里涉及了两次遍历,并且每次都要将整个fds列表传递给内核,有必定的开销。正是由于遍历操做开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
其二,进程被唤醒后,程序并不知道哪些socket收到数据,还须要遍历一次。
那么,有没有减小遍历的方法?有没有保存就绪socket的方法?这两个问题即是epoll技术要解决的。
补充说明: 本节只解释了select的一种情形。当程序调用select时,内核会先遍历一遍socket,若是有一个以上的socket接收缓冲区有数据,那么select直接返回,不会阻塞。这也是为何select的返回值有可能大于1的缘由之一。若是没有socket有数据,进程才会阻塞。
select低效的缘由之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。以下图所示,每次调用select都须要这两步操做,然而大多数应用场景中,须要监视的socket相对固定,并不须要每次都修改。epoll将这两个操做分开,先用epoll_ctl维护等待队列,再调用epoll_wait阻塞进程。显而易见的,效率就能获得提高。
select低效的另外一个缘由在于程序不知道哪些socket收到数据,只能一个个遍历。若是内核维护一个“就绪列表”,引用收到数据的socket,就能避免遍历。以下图所示,计算机共有三个socket,收到数据的sock2和sock3被rdlist(就绪列表)所引用。当进程被唤醒后,只要获取rdlist的内容,就可以知道哪些socket收到数据。
epoll是在select出现N多年后才被发明的,是select和poll的加强版本。epoll经过如下一些措施来改进效率。
原理:
以下图所示,当某个进程调用epoll_create方法时,内核会建立一个eventpoll对象(也就是程序中epfd所表明的对象)。eventpoll对象也是文件系统中的一员,和socket同样,它也会有等待队列。
建立一个表明该epoll的eventpoll对象是必须的,由于内核要维护“就绪列表”等数据,“就绪列表”能够做为eventpoll的成员。
建立epoll对象后,能够用epoll_ctl添加或删除所要监听的socket。以添加socket为例,以下图,若是经过epoll_ctl添加sock一、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。
当socket收到数据后,中断程序会操做eventpoll对象,而不是直接操做进程。
当socket收到数据后,中断程序会给eventpoll的“就绪列表”添加socket引用。以下图展现的是sock2和sock3收到数据后,中断程序让rdlist引用这两个socket。
eventpoll对象至关因而socket和进程之间的中介,socket的数据接收并不直接影响进程,而是经过改变eventpoll的就绪列表来改变进程状态。
当程序执行到epoll_wait时,若是rdlist已经引用了socket,那么epoll_wait直接返回,若是rdlist为空,阻塞进程。
假设计算机中正在运行进程A和进程B,在某时刻进程A运行到了epoll_wait语句。以下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞进程。
当socket接收到数据,中断程序一方面修改rdlist,另外一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态(以下图)。也由于rdlist的存在,进程A能够知道哪些socket发生了变化。