IO操做包括:对硬盘的读写、对socket的读写以及外设的读写。 java
一个完整的IO读请求操做包括两个阶段:编程
1)查看内核数据是否就绪;设计模式
2)进行数据拷贝(内核(内核态)将数据拷贝到用户线程(用户态))。服务器
当用户线程发起一个IO请求操做(以读请求操做为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来讲,若是数据没有就绪,则会一直在那等待,直到数据就绪。对于非阻塞IO来讲,若是数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪,当数据就绪以后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操做。网络
阻塞(blocking IO)和非阻塞(non-blocking IO)的区别: 就在于第一个阶段,若是数据没有就绪,在查看数据是否就绪的过程当中是一直等待,仍是直接返回一个标志信息。多线程
Java中传统的IO都是阻塞IO,好比经过socket来读数据,调用read()方法以后,若是数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回。异步
而若是是非阻塞IO的话,当数据没有就绪,read()方法返回一个标志信息,告知当前线程数据没有就绪,因此用户线程会一直轮询断定该返回标记等待最终的已就绪标志。socket
同步IO和异步IO模型是针对用户线程和内核的交互来讲的:async
对于同步IO:当用户发出IO请求操做以后,若是数据没有就绪,须要经过用户线程或内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;函数
而异步IO:只有IO请求操做的发出是由用户线程来进行的,IO操做的两个阶段都是由内核自动完成,而后发送通知告知用户线程IO操做已经完成。也就是说在异步IO中不会对用户线程产生任何阻塞。 因此说异步IO必需要有操做系统的底层支持!
在《Unix网络编程》一书中提到了五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。
non-blocking IO:虽然进程大部分时间都不会被block,可是它仍然要求进程去主动的check,而且当数据准备完成之后,也须要进程主动的再次调用recvfrom来将数据拷贝到用户内存。
asynchronous IO:它就像是用户进程将整个IO操做交给了他人(kernel)完成,而后他人作完后发信号通知。在此期间,用户进程不须要去检查IO操做的状态,也不须要主动的去拷贝数据。
IO multiplexing : 阻塞的用户发起IO
最传统的一种IO模型,即在读写数据过程当中会发生阻塞现象。
用户线程发出IO请求以后,内核会去查看数据是否就绪,若是没有就绪就会一直等待,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪以后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。
当用户线程发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。若是结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦内核中的数据准备好了,而且又再次收到了用户线程的请求,那么它立刻就将数据拷贝到了用户线程,而后返回。因此事实上,在非阻塞IO模型中,用户线程须要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU(弊端:可能CPU占用率会很是高) (有些像java的自旋锁)。
Java NIO实际上就是多路复用IO。
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操做,若是没有事件,则一直阻塞在那里,所以这种方式会致使用户线程(接收socket读写事件的线程)的阻塞。
优点:只须要使用一个线程就能够管理多个socket,系统不须要创建新的进程或者线程,也没必要维护这些线程和进程,而且只在真正有socket读写事件进行时,才会使用IO资源,因此大大减小了资源占用。
多路复用IO比非阻塞IO模型的效率高缘由之一是: 在非阻塞IO中,不断地询问socket状态时经过用户线程去进行的;而在多路复用IO中,轮询每一个socket状态是内核在进行的,这个效率要比用户线程要高的多。
I/O 多路复用的特色是经过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就能够返回。
不过要注意的是,多路复用IO模型是经过轮询的方式来检测是否有事件到达,而且对到达的事件逐一进行响应。所以对于多路复用IO模型来讲,一旦事件响应体很大,那么就会致使后续的事件迟迟得不处处理,而且会影响新的事件轮询。
所以: 多路复用IO比较适合链接数比较多、单个事件处理时间短而快的场景!
当用户线程发起一个IO请求操做,会给对应的socket注册一个信号函数,而后用户线程会继续向后执行;当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号以后,便在信号函数中调用IO读写操做来进行实际的IO请求操做。
异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操做以后,马上就能够开始去作其它的事。而另外一方面,从内核的角度,当它受到一个asynchronous read以后,它会马上返回,说明read请求已经成功发起了,所以不会对用户线程产生任何block。而后,内核会等待数据准备完成,而后将数据拷贝到用户线程,当这一切都完成以后,内核会给用户线程发送一个信号,告诉它read操做完成了。也就说用户线程彻底不须要实际的整个IO操做是如何进行的,只须要先发起一个请求,当接收内核返回的成功信号时表示IO操做已经完成,能够直接去使用数据了。
在异步IO模型中,IO操做的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,而后发送一个信号告知用户线程操做已完成。用户线程中不须要再次调用IO函数进行具体的读写。
这点是和信号驱动模型有所不一样的:在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,而后须要用户线程调用IO函数进行实际的读写操做;而在异步IO模型中,收到信号表示IO操做已经完成,不须要再在用户线程中调用iO函数进行实际的读写操做。
注意:
异步IO是须要操做系统的底层支持,在Java 7中,提供了Asynchronous IO。
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,由于不管是多路复用IO仍是信号驱动模型,IO操做的第2个阶段都是由用户线程来作的,内核进行数据拷贝的过程都会让用户线程阻塞。
在传统的网络服务设计模式中,有两种比较经典的模式:
一种是 多线程,一种是 线程池。
对于多线程模式,也就说来了client,服务器就会新建一个线程来处理该client的读写事件,以下图所示:
这种模式虽然处理起来简单方便,可是因为服务器为每一个client的链接都采用一个线程去处理,使得资源占用很是大
该模式的弊端:
若使用线程池,若是链接大可能是长链接,所以可能会致使在一段时间内,线程池中的线程都被占用,那么当再有用户请求链接时,因为没有可用的空闲线程来处理,就会致使客户端链接失败,从而影响用户体验。所以,线程池比较适合大量的短链接应用。
在Reactor模式中,会先对每一个client注册感兴趣的事件,而后有一个线程专门去轮询每一个client是否有事件发生,当有事件发生时,便顺序处理每一个事件,当全部事件处理完以后,便再转去继续轮询,以下图所示:
从这里能够看出,上面的五种IO模型中的多路复用IO就是采用Reactor模式。注意,上面的图中展现的 是顺序处理每一个事件,固然为了提升事件处理速度,能够经过多线程或者线程池的方式来处理事件. (netty就是分一个bossExecutor与workerExecutor)
在Proactor模式中,当检测到有事件发生时,会新起一个异步操做,而后交由内核线程去处理,当内核线程完成IO操做以后,发送一个通知告知操做已完成,能够得知,异步IO模型采用的就是Proactor模式。