深入理解IO模型中的BIO,NIO,AIO

NIO 是一种同步非阻塞,也是IO多路复用的基础,主要是解决高并发 或者 处理海量连接,IO处理问题:

比如tomcat 采用的传统的BIO(同步阻塞IO模型)+ 线程池 模式: 这个模式适合活动连接数不是特别高的(连接<1000)

这个模式是每个连接每个线程,之所以用多线程, 主要原因是在socket.accept(),socket.read(),socket.wirte() 三个函数都是同步阻塞的, 当一个连接在处理IO的时候, 系统是阻塞的,如果是单线程的话系统必然死掉, 如果是单线程的话,对于多核cpu,cpu的资源没有得到很好的利用,所以采用线程池的模式,这样线程的创建和回收成本相对较低;

如果对十万甚至百万级连接的时候,传统的BIO模型是无能为力的, 因为BIO有几个问题:

1、线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数;

2、线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半;

3、线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态;

4、容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,**大量阻塞线程从而使系统负载压力过大;

讲一下NIO模式, 所有的IO模式都分为两个阶段, 一是等待就绪(准备数据)也就是从网卡copy到内核缓存区(从内核缓存区copy到网卡), 二是真正的操作(读,写) 也就是从内核缓存区copy到用户地址空间;

前者(等待就绪)对于BIO模式是阻塞的, 对于NIO,AIO都是非阻塞的;

后者(读写处理)对于BIO,NIO都是阻塞的, 但是AIO不是阻塞的,完全是异步的, 在这个处理阶段,一般都是多核处理器,如果能够利用多核心进行I/O,无疑对效率会有更大的提高,我们可以采用线程池的模式,多个线程去处理 ,比如tomcat 的 nio就是采用此模式, 但是redis是单线程处理的,因为redis完全是内存操作,不会出现超时的现象;

工作模式:

首先:nio主要有几个事件,包括读就绪,写就绪, 新连接到来, 当有新事件操作时,首先把事件注册到对应的处理器;

其次:并由一个线程不断循环等待,调用操作系统底层的函数select() 或者 epoll(Linux 2.6之前是select、poll,2.6之后是epoll,Windows是iocp),并负责向操作系统查询IO是否就绪(标记:从网卡已经拷贝到内核缓存区,准备就绪),如果就绪执行事件处理器(从内核缓存区到用户内存);

这个过程就是利用了Reactor事件驱动的模式;

select 与 epoll的区别:

1、每次调用select,都要把fd_set(加入文件描述符至集合)从用户态拷贝到内核态,fd_set很大时这是费时操作

2、每次调用select,内核态都要遍历fd_set,fd_set很大时这是费时操作, epoll 如果准备就绪,系统回调通知,有个回调函数;

3、select支持的文件描述符数量太小,默认是1024, epoll没有限制(系统最大的文件句柄数)

流程图如:

深入理解IO模型中的BIO,NIO,AIO

NIO存在的问题

使用NIO != 高性能,当连接数<1000,并发程度不高或者局域网环境下NIO并没有显著的性能优势。

NIO并没有完全屏蔽平台差异,它仍然是基于各个操作系统的I/O系统实现的,差异仍然存在。使用NIO做网络编程构建事件驱动模型并不容易,陷阱重重。

推荐大家使用成熟的NIO框架,如Netty,MINA等。解决了很多NIO的陷阱,并屏蔽了操作系统的差异,有较好的性能和编程模型。

IO的发展历程

JDK 1.4之前是普通IO,JDK 1.4中引入的NIO, JDK1.7引入NIO2.0(AIO)