Java中的IO方式
主要分为3种:BIO(同步阻塞)、NIO(同步非阻塞)和AIO(异步非阻塞)。
BIO
同步阻塞模式。在JDK1.4以前,使用Java建立网络连接时,只能采用BIO方式,在服务器端启动一个ServerSocket,然后使用accept等待客户端请求,对于每一个请求,使用一个线程来进行处理用户请求。线程的大部分时间都在等待请求的到来和IO操作,利用率很低。而且线程的开销比较大,数量有限,因此服务器同时能处理的连接数也很低。
NIO
BIO模式中,是“一个Socket一个线程”;而在NIO中则是使用单个或少量的线程来轮询Socket,当发现Socket上有请求时,才为请求分配线程。因此是“一个请求一个线程”。
具体实现就是把Socket通过Channel注册到Selector,使用一个线程在Selector中轮询,发现Channel有读写的事件,就可以分配给其他线程来处理(通常使用线程池)。
AIO
从JDK7开始支持AIO模式。通过AsynchronousServerSocketChannel中注册事件回调函数来处理业务逻辑。当IO操作完成以后,回调函数会被调用。如果传入AsynchronousChannelGroup,可以绑定线程池来处理事件。
关于JDK的实现,Windows平台基于IOCP实现AIO,Linux只有eppoll模拟实现了AIO。
用一句话来总结这三种IO的区别:
- BIO是一个连接一个线程。
- NIO是一个请求一个线程。
- AIO是一个有效请求一个线程。
IO中的几个概念
以银行取款为例:
- 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);
- 异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API);
- 阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);
- 非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)
系统I/O 可分为阻塞型, 非阻塞同步型以及非阻塞异步型。
阻塞型I/O意味着控制权只到调用操作结束了才会回到调用者手里。 结果调用者被阻塞了, 这段时间了做不了任何其它事情。 更郁闷的是,在等待IO结果的时间里,调用者所在线程此时无法腾出手来去响应其它的请求,这真是太浪费资源了。拿read()操作来说吧, 调用此函数的代码会一直僵在此处直至它所读的socket缓存中有数据到来。
相比之下,非阻塞同步是会立即返回控制权给调用者的。调用者不需要等等,它从调用的函数获取两种结果:要么此次调用成功进行了;要么系统返回错误标识告诉调用者当前资源不可用,你再等等或者再试度看吧。比如read()操作, 如果当前socket无数据可读,则立即返回EWOULBLOCK/EAGAIN,告诉调用read()者”数据还没准备好,你稍后再试”。
在非阻塞异步调用中,稍有不同。调用函数在立即返回时,还告诉调用者,这次请求已经开始了。系统会使用另外的资源或者线程来完成这次调用操作,并在完成的时候知会调用者(比如通过回调函数)。拿Windows的ReadFile()或者POSIX的aio_read()来说,调用它之后,函数立即返回,操作系统在后台同时开始读操作。
在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。
Reactor and Proactor
一般情况下,I/O 复用机制需要事件分享器(event demultiplexor )。 事件分享器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁的什么东西送了, 快来拿吧。开发人员在开始的时候需要在分享器那里注册感兴趣的事件,并提供相应的处理者(event handlers),或者是回调函数; 事件分享器在适当的时候会将请求的事件分发给这些handler或者回调函数。
涉及到事件分享器的两种模式称为:Reactor and Proactor 。 Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。 在Reactor模式中,事件分离者等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分离者就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
Reactor模式:
Reator类图如上所示,Reactor模式又叫反应器或反应堆,即实现注册描述符(Handle)及事件的处理器(EventHandler),当有事件发生的时候,事件多路分发器(Event Demultiplexer)做出反应,调用事件具体的处理函数(ConcreteEventHandler::handle_event())。
Reator模式的典型启动过程如下:
- 创建Reactor
- 注册事件处理器(Reactor::register_handler())
- 调用事件多路分发器进入无限事件循环(Reacor:handle_events)
- 当操作系统通知某描述符状态就绪时,事件多路分发器找出并调用此描述符注册的事件处理器。
Reactor模式已经被广泛使用,著名的开源事件库libevent、libev、libuv都是使用Reactor模式。
Proactor模式
Proactor模式的类图如上图所示,Proactor模式又叫前摄器或主动器模式。它用于实现异步I/O模型,运行流程如下:
- Initiator主动调用Asynchronous Operation Processor发起异步I/O操作,
- 记录异步操作的参数和函数地址放入完成事件队列(Completion Event Queue)中
- Proactor循环检测异步事件是否完成。如果完成则从完成事件队列中取出回调函数完成回调。
Boost库中的asio就使用了Proactor模式,其底层的异步I/O由操作系统提供,而异步事件的分发还是由epoll/kequeue/select等实现。
在Proactor模式中,事件处理者(或者代由事件分离者发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区,读的数据大小,或者用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分离者得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。
两者区别
主要区别:
- Reactor实现同步I/O多路分发,Proactor实现异步I/O分发。
如果只是处理网络I/O单线程的Reactor尚可处理,但如果涉及到文件I/O,单线程的Reactor可能被文件I/O阻塞而导致其他事件无法被分发。所以涉及到文件I/O最好还是使用Proactor模式,或者用多线程模拟实现异步I/O的方式。
- Reactor模式注册的是文件描述符的就绪事件,而Proactor模式注册的是完成事件。
即Reactor模式有事件发生的时候要判断是读事件还是写事件,然后用再调用系统调用(read/write等)将数据从内核中拷贝到用户数据区继续其他业务处理。
而Proactor模式一般使用的是操作系统的异步I/O接口,发起异步调用(用户提供数据缓冲区)之后操作系统将在内核态完成I/O并拷贝数据到用户提供的缓冲区中,完成事件到达之后,用户只需要实现自己后续的业务处理即可。
- 主动和被动
Reactor模式是一种被动的处理,即有事件发生时被动处理。而Proator模式则是主动发起异步调用,然后循环检测完成事件。
写在最后
我们知道linux系统提供的异步I/O,只支持O_DIRECT,不能带缓存。因此出现了开源库libeio,它和Linux的异步I/O一样也是用多线程模拟,但是更高效。下图是libeio的异步I/O实现,是不是很像Proactor模式啊。