在JavaIO中,提供了3种IO,分别是BIO,NIO和AIO。在学习这三个之前,我们需要先了解一些概念。
IO操作
我们知道,一个程序需要经常读取一些外设的信息,如硬盘,显卡上的信息,这些操作被称为IO操作,以读操作为例,IO操作可以被分为两个阶段:
①查看数据是否准备就绪,
②进行数据拷贝。
对于计算机来说,IO操作是非常耗时的,应为CPU跟外设之间的速度极度不匹配,相对CPU的速度来说,IO操作是非常慢的。
同步/异步
在同步IO下,需要不断轮询数据是否准备就绪,当数据准备就绪时,再将数据拷贝到用户线程。在IO操作发生时,直到IO操作完成之前,整个进程都会被阻塞。也就是说,多个IO同时发生时,其中任何一个IO操作都会阻塞其他IO操作。
在异步IO下,一个线程请求IO操作,并不会导致线程被阻塞,在用户发出请求后,便不再管它,IO操作由内核自动完成,然后由内核发送通知告诉用户线程IO操作已完成,多个IO同时发生时,都同时进行,互不阻塞。
区别
它们之间的区别便是在IO操作第二个阶段中,数据拷贝是否需要进程参与。
在需要进程参与数据拷贝的同步IO下,无疑需要消耗进程的时间片(即CPU分配给进程的一段时间),在这段时间内进程只是等待IO操作的完成,无法做其他事情。
在不需要进程参与数据拷贝的异步IO下,进程得到了释放,可以自己做其他事情不用傻傻地在等待IO操作完成。
阻塞/非阻塞
当一个程序发出一个IO操作请求时,如果该请求得不到满足(即对应的设备繁忙),针对情况的两种表现便是阻塞和非阻塞的区别。在阻塞情况下,进程会在这里一直等待(即进程阻塞),直到对应设备能够重新工作。在非阻塞情况下,会立刻返回一个标志信息告知不能满足该请求。
区别
它们之间的区别便是在IO操作的第一个阶段中,如果数据没有就绪时是选择继续等待还是选择立刻返回一个标志,让CPU能继续执行其他工作。
理解了以上概念之后,就可以开始BIO/NIO/AIO的学习。
BIO
Block IO,阻塞IO,是一种同步阻塞型的IO,在JDK1.4以前这是JAVA中的唯一选择。
原理:
在服务器端调用accept()方法等待客户端的连接请求,如果没有连接则一直阻塞在这里。一旦有客户端进行连接,便建立通信套接字进行读写操作,一般情况下,此时不能接受其他客户端的连接,只能够等待当前IO完成后才可以继续下一个连接。为了可以处理多用户,则必须用到多线程,一个请求过来,服务器便分配给它一个线程对客户请求进行处理。这种一一对应的处理方式,当客户变多的时候,线程的开销是巨大的,系统性能将急剧下降。 而且,当一个线程处理IO需要的时间很长的时候,其他的线程还未被服务的线程可能就要等待很久了。
适用场景:
BIO方式适用于连接数目比较小且固定的场景,这种方式对服务器资源要求比较高,并发局限于应用中。
NIO
No-block IO,非阻塞IO,在JDK1.4之后提供了API。特征是同步非阻塞,一个IO请求一个线程。
原理:
NIO采用了事件驱动的思想,用单个线程来提供了BIO中用多线程方式提供的服务。
它使用了一种高性能IO设计模式:Reactor模式。利用一个叫做多路复用器Selector的线程来监听所有客户端的IO事件,如客户端的连接请求,客户端通过套接字发送数据这个write事件,或者客户端要求从服务器端得到某些数据的read事件,这些都由同一个线程进行管理,它负责在这些事件到达的时候触发。这个线程不停地轮询,直到有IO请求时才启动一个新线程进行处理。这样的话,服务器不用每次有新客户端连接就分配给它一个线程,而只需要在多路复用器上进行注册就可以进行管理。并且只有在真正的IO事件发生时,才会分配线程去执行IO事件,这种设计模式大大减少了资源占用。
在NIO中所有的数据都利用缓冲区Buffer来处理,访问和写入NIO,都是通过Buffer这个数据结构。而我们对Buffer的访问要通过通道Channel,它不同于单向的流的地方便是它是全双工的,可以同时用于写操作和读操作。上面提到的多路复用器Selector会不断轮询注册在上面的Channel,如果Channel发生IO操作,这个Channel就处于就绪状态,可能被Selector读取出来,获取Channel的集合进行后续的IO操作。
下面通过一段代码更好地理解NIO。
while (true) { // select()阻塞,等待有事件发生唤醒 int selected = selector.select(); if (selected > 0) { Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator(); while (selectedKeys.hasNext()) { SelectionKey key = selectedKeys.next(); if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) { // 处理 accept 事件 } else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) { // 处理 read 事件 } else if ((key.readyOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) { // 处理 write 事件 } selectedKeys.remove(); } } }
我们知道NIO是同步非阻塞的,int selected = selector.select(); 这句代码中,它却是阻塞到有事件发生才唤醒,即事件驱动。这个是否与NIO同步非阻塞的特点前后矛盾了呢?在这里我们复习一下同步和阻塞的概念,其实这里的select的阻塞,针对的是IO操作的第一个阶段,也就是说,其实这是同步的一种表现。在同步IO下,需要不断轮询数据是否准备就绪,当数据准备就绪时,再将数据拷贝到用户线程。我们可以看到,针对accept,write,read等事件的操作便是非阻塞的,它针对的是IO操作的第二阶段,数据拷贝阶段。
NIO适用场景:
适合处理连接数目特别多,但是连接比较短(轻操作)的场景
AIO:
异步IO,从JDK7开始支持。特征是异步非阻塞,一个有效IO一个线程。需要操作系统支持异步IO。
原理:
使用了高性能IO设计模式:Proactor模式。跟Reactor模式类似,但它是异步非阻塞的,也就是说,当检测到有事件发生时,会新起一个异步操作,然后交由内核线程去处理,当内核线程完成IO操作之后,发送一个通知告知操作已完成,在这里事件处理器关注读取完成事件而不是读取就绪事件
使用说明:
read/write方法都是异步的,完成后会主动调用回调函数。当进行读写操作时,只须直接调用API的read或write方法即可。在这种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,真正的IO读取或者写入操作已经由内核完成了。
适用场景:
AIO方式使用于连接数目多且连接比较长(重操作)的架构,充分调用OS参与并发操作。