在本篇文章中,我们主要介绍一下java中的BIO NIO AIO,重点是NIO
先说一下同步、异步、阻塞和非阻塞。
简单来讲,同步和异步是针对内核和应用程序之间的交互而言的;阻塞和非阻塞其实是针对进程在访问数据时,根据IO操作的就绪状态采取的不用方式(就是读取/写入函数的实现方式)。
同步/异步是宏观上(进程间通讯,通常表现为网络IO的处理上)的,阻塞和非阻塞是微观上(进程内的数据传输,通常表现为对本地IO的处理上)的。
阻塞和非阻塞其实是同步和异步的表现形式。也就是说,同步和异步是目的,阻塞和非阻塞是实现方式。
同步和异步的区别在于是否有通知;阻塞和非阻塞的区别在于程序在等待调用结果时的(消息、返回值)的状态。
在Linux IO模型中:
阻塞/非阻塞: 等待I/O完成的方式,阻塞要求用户程序停止执行,直到IO完成,而非阻塞在IO完成之前还可以继续执行;
同步/异步: 获知IO完成的方式,同步需要时刻关心IO是否完成,异步无需主动关心,在IO完成时它会收到通知。
其实一个IO 操作分为两个步骤:发起IO请求和实际的IO操作。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞。
同步阻塞: BIO Blocking IO Input/Output
在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式。
同步非阻塞: NIO Nonblocking IO(New IO)
在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。
异步: AIO Asynchronous IO
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序。”
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器(selector)上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问。
NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知应用程序进行处理,应用再将流读取到缓冲区或写入操作系统。
NIO中,同步的核心是Selector,非阻塞的核心是通道和缓冲区。
当IO事件就绪时,可以通过通道写入缓冲区,无需线程阻塞式的等待(因为连接建立之后,IO的数据未必会马上到达,在BIO中,等待IO的线程要阻塞,等待执行IO操作)。
引用 Java NIO 中权威的说法:通道是 I/O 传输发生时通过的入口,而缓冲区是这些数 据传输的来源或目标。对于离开缓冲区的传输,您想传递出去的数据被置于一个缓冲区,被传送到通道。对于传回缓冲区的传输,一个通道将数据放置在您所提供的缓冲区中。
当执行:SocketChannel.write(Buffer),便将一个 buffer 写到了一个通道中。
在NIO中:如果想将Data发到目标端,则需要将存储该Data的Buffer,写入到目标端的Channel中,然后再从Channel中读取数据到目标端的Buffer中。
Selector是同步的核心,用于监管IO事件。
Selector允许单个线程处理多个Channel。若 应用程序打开了多个Channel,但是每一个连接的流量都很低,使用Selector就很方便。
使用selector的话,就要向Selector注册Channel,然后调用它的select()方法,该方法就一直阻塞到某个注册的通道有事件发生,这就是所说的轮询。一旦select方法返回,线程就可以处理这些事件。
Selector中注册的感兴趣的事件有:
OP_ACCEPT
OP_CONNECT
OP_READ
OP_WRITE
我们可以进行优化:将selector进一步分解为Reactor,将不同的感兴趣的事件分开,每一个Reactor只负责一种感兴趣的事件。
好处就是:
a.分离阻塞级别,减少轮询的时间;
b.线程无需遍历set以找到自己感兴趣的事件,因为得到的set中仅包含自己感兴趣的事件。
关于Reactor的相关介绍见我的另一篇文章:《IO之 Reactorm模式》
HTTP/1.1出现后,有了Http长连接,这样除了超时和指明特定关闭的http header外,这个链接是一直打开的状态的,这样在NIO处理中可以进一步的进化,在后端资源中可以实现资源池或者队列,当请求来的话,开启的线程把请求和请求数据传送给后端资源池或者队列里面就返回,并且在全局的地方保持住这个现场(哪个连接的哪个请求等),这样前面的线程还是可以去接受其他的请求,而后端的应用的处理只需要执行队列里面的就可以了,这样请求处理和后端应用是异步的.当后端处理完,到全局地方得到现场,产生响应,这个就实现了异步处理。
一个有效请求一个线程。客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel
epoll是Linux内核的IO模型。我想一定有人想问,AIO听起来比NIO更加高大上,为什么不使用AIO?AIO其实也有应用,但是有一个问题就是,Linux是不支持AIO的,因此基于AIO的程序运行在Linux上的效率相比NIO反而更低。而Linux是最主要的服务器OS,因此相比AIO,目前NIO的应用更加广泛。
说到这里,可能你已经明白了,epoll一定和NIO有着很深的因缘。没错,如果仔细研究epoll的技术内幕,你会发现它确实和NIO非常相似,都是基于“通道”和缓冲区的,也有selector,只是在epoll中,通道实际上是操作系统的“管道”。和NIO不同的是,NIO中,解放了线程,但是需要由selector阻塞式地轮询IO事件的就绪;而epoll中,IO事件就绪后,会自动发送消息,通知selector:“我已经就绪了。”可以认为,Linux的epoll是一种效率更高的NIO。
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?
因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(如果从UNP的角度看,select属于同步操作。因为select之后,进程还需要读写数据),从而提高系统的并发性!
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于**连接数目多且连接比较长(重操作)**的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
应用场景:并发连接数不多时采用BIO,因为它编程和调试都非常简单,但如果涉及到高并发的情况,应选择NIO或AIO,更好的建议是采用成熟的网络通信框架Netty。
部分转自:https://www.cnblogs.com/geason/p/5774096.html