一、IO模型有哪些?
IO模型主要分为三类:BIO、NIO和AIO。
Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装。程序员在使用这些 API 的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要使用Java的API就可以了。
二、BIO
BIO是同步并阻塞IO,服务器实现模式为一个连接一个线程,即客户端有链接请求时服务器端就需要启动一个线程进行处理,如果这个链接不做任何事情会造成不必要的线程开销。这样做的弊端是,如果服务器收到大量来自客户端的IO请求时,就需要大量的线程资源来处理这些请求,服务器资源很有可能被耗尽,高并发的情况下可能会导致大量的连接被挂起,服务器资源会严重不足;并且因为是同步阻塞模型,所以每个socket连接都需要服务器新建线程,哪怕是使用了线程池,也会因为线程之间的切换造成资源的损耗。
之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。其实这也是所有使用多线程的本质: 1. 利用多核。 2. 当I/O阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源。
不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很”贵”的资源,主要表现在: 1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。 2. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。 3. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。 4. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
图片来自PHP中文网
三、NIO
NIO是同步非阻塞IO,服务器的实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。用户进程也需要时不时询问IO操作是否就绪,这就要求用户进程不停地去询问。由于所有读写操作都是由acceptor进行分发的,所以效率瓶颈主要在于acceptor。
回忆BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,一是没有办法知道到底能不能写、能不能读,只能”傻等”,即使通过各种估算,算出来操作系统没有能力进行读写,也没法在socket.read()和socket.write()函数中返回,这两个函数无法进行有效的中断。所以除了多开线程另起炉灶,没有好的办法利用CPU。
而NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。
图片来自美团技术团队
四、AIO
AIO是异步非阻塞IO,在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。AIO需要一个连接去注册读写事件和回调方法,也就是说当读写操作进行时,我们只需要调用API中的read和write方法就行了。
缺点:
1、实现复杂。代码量太庞大;
2、需要额外的技能。也就是说你想要学号AIO,还需要java多线程的技术做铺垫才可以。否则我们很难写出质量高的代码。
3、一个著名的Selector空轮询bug。它会导致CPU100%,之前在我的群里面,有人曾经遇到过这个问题,而且官方说在1.6的版本中解决,但是现在还有。遇到的时候我们虽然可以解决但是不知道的人会很痛苦。
4、可靠性差。也就是说我们的网络状态是复杂多样的,会遇到各种各样的问题,比如说网断重连、缓存失效、半包读写等等。可靠性比较差。稍微出现一个问题,还需要大量的代码去完善。
五、适用场景
BIO适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,但程序直观简单容易理解,jdk1.4之前的唯一选择;
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持;
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS并发操作,编程比较复杂,JDK1.7开始支持。
参考: