1、阻塞IO模型
传统的IO模型,即在数据读写的过程中会发生阻塞现象。当用户线程发出IO请求之后,内核回去查看数据是否就绪,如果没有就绪就等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回数据给用户线程,用户线程才解除阻塞状态。典型的阻塞IO模型的例子:data = socket.read();如果数据没有就绪,就会一直阻塞。
2、非阻塞IO模型
当用户发出一个read请求之后,不需要等待,会立马得到一个结果,如果这个结果是错误的,它就是知道数据还没有就绪,于是再次发送read操作。一旦内核中的数据准备就绪,又再次收到用户线程的请求,它就立马将数据拷贝给用户线程,然后返回。所以在非阻塞IO模型中,因为用户线程需要不断询问内核数据是否准备就绪,所以整个过程中用户线程不会交出CPU,而会一直占用CPU。典型的非阻塞IO模型一般如下:
while(true){ data = socket.read(); if(data!= error){ //do something ... break; } }
这种方式有一个非常严重的问题就是在while循环中用户线程不断循环询问数据是否就绪,导致CPU占用率非常高,因此一般不回使用while循环这种方式读取数据。
3、多路复用IO模型
首先解释一下什么是多路复用,多路复用是一种同步IO模型,实现一个线程可以监听多个文件句柄,一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作。没有文件句柄就绪时会阻塞应用程序,交出CPU。多路是指网络链接,复用指的是同一个线程。
Java中的NIO实际就是多路复用IO。在多路复用IO中,会有一个线程不断去轮询多个socket的状态,只有当这个socket真正读写事件时,才真正调用实际的IO读写操作。因此在多路复用IO中,只需要一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必去维护这些进程和线程,并且只有在真正有IO读写操作时,才会调用IO操纵,大大减少了资源的占用。在Java NIO中,是通过selector.selct()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在哪里,因此这种方式会导致用户线程的阻塞。所以多路复用比较适合连接数比较多的情况。另外多路复用IO模型和非阻塞IO模型相比效率高的原因是在非阻塞IO模型中,不断循环询问socket状态是通过用户线程去进行的。而在多路复用IO模型中,轮询每个socket状态是在内核中进行的。
多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件 逐一进行响应。因此对于多路复用 IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件 迟迟得不到处理,并且会影响新的事件轮询。
4、信号驱动IO模型
在信号驱动IO模型中,当用户线程发起一个IO请求操作后,会给对应的socket注册一个信号函数,然后用户线程继续执行。当内核数据准备就绪之后,会发送一个信号给用户线程,用户线程接收到信号函数之后,便在信号函数中调用IO操作进行实际的IO请求操作。
5、异步IO模型
异步IO操作模型中,用户线程发起一个read请求之后,立刻就可以去做其他的事。另外从内核角度,当它收到一个asynchronous read之后,会立即返回,说明read请求发起成功了,不会对用户线程有任何阻塞。然后当内核数据将数据准备就绪之后,会将数据拷贝到用户线程,当一切都完成之后,发送一个信号给用户线程说read请求完成了。也就是说用户线程不用知道实际的IO操作是如何进行的,只需要发送一个read请求,当接收到内核线程的信号时表示IO操作已完成,可以直接使用数据了。
6、Java NIO
NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。
NIO和IO最大的区别就是NIO是面向缓冲区的,IO是面向流的。NIO核心主要是三大部分:Channel、Buffer、Selector。传统IO是通过字符流和字节流进行操作,而NIO是通过channel和Buffer进行操作。Selector用于监听多个通道事件(比如连接打开、数据到达)。单线程可以监听多个通道。
首先说一下 Channel,国内大多翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个 等级的。只不过 Stream 是单向的,譬如:InputStream, OutputStream,而 Channel 是双向 的,既可以用来进行读操作,又可以用来进行写操作。NIO 中的 Channel 的主要实现有:
上面的图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送 数据时,必须先将数据存入 Buffer 中,然后将 Buffer 中的内容写入通道。服务端这边接收数据必 须通过 Channel 将数据读入到 Buffer 中,然后再从 Buffer 中取出数据来处理。
在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类,常用的 Buffer 的子类有: ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、 ShortBuffer
Selector 类是 NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事 件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可 以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用 函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护 多个线程,并且避免了多线程之间的上下文切换导致的开销。