NIO在jdk1.7之前是同步非阻塞IO,在jdk1.7之后是异步非阻塞IO叫做AIO。
今天我们就重点讲解下非阻塞的概念。通常我们在网络相关的地方才使用到非阻塞的概念。
- 当我们建立网络套接字的时候什么时候服务器端的socket会等待?
(1)当客户端没有连接过来的时候,我们的服务端会调用serversocket的accept()方法进行等待,等待客户端的连接,该步骤就是阻塞式的,该IO就是传统的IO,只有当客户端建立连接的时候,该步骤才会继续执行,这种就是同步阻塞IO
(2)客户端与服务端建立连接后,客户端等待服务端的数据,或者服务端等待客户端的数据,这个步骤也是阻塞的。 - 如何解决这种同步阻塞呢?
利用伪异步阻塞IO,就是使用多线程对此来实现。该方式没有真正的解决阻塞IO的核心,其本质还是阻塞的,只不过用多线程让其进行切换。但是使用该方法的时候,会使线程过多,CPU内存使用过多。针对这个问题我们可以使用线程池对其进行解决。
上面的方法不论怎么解决其实还是同步阻塞IO
- IO是分为两个部分的分别是用户态和内核态
(1)数据通过用户态到达内核态
(2)内核态转备好数据的时候,从内核态写入用户态 - 同步
第二步数据从内核态写入用户态是用户的线程自行读取数据,处理数据的 - 异步
第二步数据是内核态负责写入用户态的,他只是告诉用户我的数据已经准备好了,并且读取完成,这时候直接调用用户态的线程就可以取到数据,内核已经帮我们完成了数据的准备,以及将数据从内核态写入到用户态。
我们可以这么理解,阻塞是说明当数据没有准备好的时候我什么事情都不做,一直等待数据,直到数据准备完成后才去执行下面的步骤;而非阻塞是我们数据没来,我们做自己的事情,有一个进程不停的帮我们去轮训检测数据是否准备完毕,直到数据来了,我们才会接到通知,继续执行。
BIO
BIO就是我们说的IO操作,是同步阻塞IO
从上面的图我们可以看出来就是说我们用户态的线程发送请求读取数据的时候,会一直阻塞,等待内核态将数据准备好,并且将准备好的数据拷贝到用户线程,这时候用户线程才能继续操作,在内核态准备数据和拷贝数据的阶段,用户线程都是阻塞的。这就是我们最常用的IO,也就是BIO的运行方式。
NIO(同步非阻塞IO)
通过上面的图我们可以很明显的看到,当应用层发送一个请求到内核态的时候,如果内核态没有数据准备好,我们不会让应用层即用户态继续等待,而是直接发送给他一个EWOULDBLOCK的错误,这样我们应用层就可以一直轮训判断这个状态,借此来检测内核态是否准备好了数据,但是这里的非阻塞只是在内核态数据准备的时候是非阻塞的,内核态将准备好的数据复制到用户态,应用层的时候依然是阻塞的。
IO复用模型
IO复用模型是指的是Linux提供了select/poll的操作,进程通过将一个或者多个fd传递给select或者poll系统调用,阻塞在select操作上,这样子select/poll可以帮我们检测是否有一个或者多个fd是就绪状态,是就绪状态表示的是内核态度的数据已经准备完成。当检测出是就绪状态的时候,立刻调用回调函数。这时候应用层再向内核态请求数据的时候,数据已经是准备好的,因此也无需进行阻塞,但是数据从内核复制到用户空间的时候还是需要进行短暂阻塞的。
poll和epoll的区别
poll支持的fd数量是有限的,且select/poll是顺序扫描fd是否就绪
epoll是使用基于事件驱动方式代替的顺序扫描,因此性能高,且epoll的fd数量远大于poll。
信号驱动IO模型
我们上面讲的IO多路复用,可以看到select/poll的时候应用程序一直是阻塞的,直到内核态的数据准备完成之后,我们才会调用其回调函数进行下面的操作,那我们的信号驱动IO模型则更进一步的优化。
信号驱动IO模型,首先开启套接口信号驱动IO功能,并且通过系统调用sigaction执行一个信号处理函数(此系统调用之后立即返回,进程继续工作,他是非阻塞的)。当数据准备就绪的时候,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,处理数据,同样的内核态的数据复制到用户态的时候依然是阻塞的。
异步IO
异步IO解决的最大的问题就是内核态数据复制到用户态的时候,我们也是非阻塞的,这个是怎么实现的呢?
异步IO,告知内核启动某个操作,并让内核在整个操作完成之后(包括将数据从内核态复制到用户态)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动IO由内核通知我们什么时候开始一个IO操作 ;异步IO模型由内核通知我们IO的操作何时完成。