用生活中最常见的煮开水举例。出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
所谓同步异步,只是对于水壶而言。普通水壶——同步;响水壶——异步。虽然都能干活,但响水壶可以在自己完工之后,通知老张水开了。这是普通水壶所不能及的。同步只能让调用者去自己去查看而不会通知(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。等待的老张——阻塞;看电视的老张——非阻塞。虽然响水壶是异步的,可对于等待的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
同步和异步是针对应用程序和内核的交互而言的,
同步指的是用户进程触发IO 操作并等待或者 轮询的去查看 IO 操作是否就绪,
异步是指用户进程触发IO 操作以后便开始做自己的事情,而当IO 操作已经完成的时候会得到IO 完成的 通知。
所以同步异步重点在于是否有通知,有通知就是异步,没有通知,必须自己去查看就是同步。 还有一种说法是返回时机,同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。
阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入方法会立即返回一个状态值。
阻塞和非阻塞在于是否需要等待,必须自己等待就是阻塞住了,自己不能去做其他事情。而非阻塞就是自己可以去做其他事情,不用在这等着。
所以非阻塞配合异步才有意义,比如去银行存钱,人多我先排个号,然后自己去干其他事情,然后需要安排个熟人在银行,说到我了你打电话通知我。
根据以上例子我们来帮助我们理解最经典的Unix网络IO模型,其实这也是Unix内核使用的系统io模型,之所以叫网络IO我觉得是因为网络和系统他们都是同一套模型,及网络有客户端和服务器,而系统对应的就是进程和内核处理器。
也就是同步阻塞,进程会一直阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。 我们第一次接触到的网络编程都是从 listen()、send()、recv()等接口开始的。使用这些接口可以很方便的构建服务器 /客户机的模型。
最常用的I/O模型就是阻塞IO模型,缺省情况下,所有文件操作都是阻塞的。
我们以套接字接口为例来讲解此模型:在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直会等待。进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此称为阻塞IO模型。
阻塞模式给网络编程带来了一个很大的问题,如在调用 send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这时,我们可能会选择多线程的方式来解决这个问题。
应对多客户机的网络应用,最简单的解决方式是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回);
recvfrom从应用到内核的时候,如果该缓冲区没有数据准备好的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞IO模型进行轮询检查这个状态,看内核是不是有数据到来。这个不断轮训测试过程会大量的占用CPU的时间
也就是说是同步非阻塞io模型,因为没有通知,所以需要应用自己去询问。
在数据拷贝的过程中,进程是阻塞的。
由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而 “后台” 可能有多个任务在同时进行。如果有人帮忙轮询就好了。注意这里还是自己轮询,而不是内核来通知。所以是同步到。那么这就是所谓的 “IO 多路复用”。也是同步I/O,内核拷贝数据到应用时,会阻塞进程。
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的 IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
TCP服务器要同时处理监听socket和连接socket。这是I/O多路复用使用最多的场合。
相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。
告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动模型由内核通知我们何时可以开始一个IO操作;异步IO模型由内核通知我们IO操作何时完成。
由于信号驱动io用的很少,这里我们只着重对比四种io,如下图
根据以上几种情况我们对比举例:
用的最多的应该就是io多路复用技术,而且效率最高,可以实现一个内核处理多个线程任务。
Unix中的五种I/O模型,除信号驱动I/O外,Java对其它四种I/O模型都有所支持。
Blocking IO: 同步阻塞的编程方式。
BIO编程方式通常是在JDK1.4版本之前常用的编程方式。编程实现过程为:首先在服务端启动一个ServerSocket来监听网络请求,客户端启动Socket发起网络请求,默认情况下ServerSocket回建立一个线程来处理此请求,如果服务端没有线程可用,客户端则会阻塞等待或遭到拒绝。
且建立好的连接,在通讯过程中,是同步的。在并发处理效率上比较低。大致结构如下:
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题,NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。
NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。
在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题
通过NIO实现的Reactor模式即是I/O多路复用模型的实现
通过AIO实现的Proactor模式即是异步I/O模型的实现
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel