讲解IO思路:
BIO(一个连接一个线程)
-->大并发问题-->NIO(操作系统层面:IO多路复用)
-->NIO两个问题:1.谁去监听就绪(Boss),2.谁来处理已就绪(Work)
-->AIO:NIO的两个问题都不存在:1.每一个IO操作注册回调函数(异步),不需要多路复用器去监听就绪事件(监听完成事件?),2.无需处理IO操作,操作系统完成
简而言之,NIO的多路复用器,是通知你IO就绪事件,AIO的回调是通知你IO完成事件。AIO做的更加彻底一些。
反应器Reactor
主动器Proactor
Reactor模式下的IO操作,是在应用进程中执行的,Proactor中的IO操作是由操作系统来做的
主动和被动
以主动写为例:
Reactor将handle放到select(),等待可写就绪,然后调用write()写入数据;写完处理后续逻辑;
Proactor调用aoi_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑;
可以看出,Reactor被动的等待指示事件的到来并做出反应;它有一个等待的过程,做什么都要先放入到监听事件集合中等待handler可用时再进行操作;
Proactor直接调用异步读写操作,调用完后立刻返回;
Reactor模式
Reactor模式的处理:服务器端启动一条单线程,用于轮询IO操作是否就绪,当有就绪的才进行相应的读写操作,这样的话就减少了服务器产生大量的线程,解决了BIO的问题。(目前JAVA的NIO就采用的此种模式)
Proactor模式
运用于异步I/O操作,Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.
二、Java中的典型IO操作模式
2.1 同步阻塞模式
Java中的BIO风格的API,都是该模式,例如:
Socket socket = getSocket(); socket.getInputStream().read(); //读不到数据誓不返回
该模式下,最直观的感受就是如果IO设备暂时没有数据可供读取,调用API就卡住了,如果数据一直不来就一直卡住。
2.2 同步非阻塞模式
Java中的NIO风格的API,都是该模式,例如:
SocketChannel socketChannel = getSocketChannel(); //获取non-blocking状态的Channel socketChannel.read(ByteBuffer.allocate(4)); //读不到数据就算了,立即返回0告诉你没有读到
该模式下,通常需要不断调用API,直至读取到数据,不过好在函数调用不会卡住,我想继续尝试读取或者先去做点其他事情再来读取都可以。
2.3 异步非阻塞模式
Java中的AIO风格的API,都是该模式,例如:
AsynchronousSocketChannel asynchronousSocketChannel = getAsynchronousSocketChannel(); asynchronousSocketChannel.read(ByteBuffer.allocate(4), null, new CompletionHandler<Integer, Object>() { @Override public void completed(Integer result, Object attachment) { //读不到数据不会触发该回调来烦你,只有确实读取到数据,且把数据已经存在ByteBuffer中了,API才会通过此回调接口主动通知您 } @Override public void failed(Throwable exc, Object attachment) { } });
该模式服务最到位,除了会让编程变的相对复杂以外,几乎无可挑剔。
三、分离快与慢
3.1 BIO的局限
一个连接一个线程,无法处理大并发问题
3.2 NIO的突破
3.2.1 突破思路
由于NIO的非阻塞特性,决定了IO未就绪时,线程可以不必挂起,继续处理其他事情。这就为分离快与慢提供了可能,高速的CPU和内存可以不必苦等IO交互,一个线程也不必局限于只为一个IO连接服务。这样,就让用少量的线程处理海量IO连接成为了可能。
3.2.2 思路落地
虽然我们看到了曙光,但是要将这个思路落地还需解决掉一些实际的问题。
a)当IO未就绪时,线程就释放出来,转而为其他连接服务,那谁去监控这个被抛弃IO的就绪事件呢?(BOSS线程进行监控)
b)IO就绪了,谁又去负责将这个IO分配给合适的线程继续处理呢?(Work线程处理IO)
为了解决第一个问题,操作系统提供了IO多路复用器(比如Linux下的select、poll和epoll),Java对这些多路复用器进行了封装(一般选用性能最好的epoll),也提供了相应的IO多路复用API。NIO的多路复用API典型编程模式如下:
// 开启一个ServerSocketChannel,在8080端口上监听 ServerSocketChannel server = ServerSocketChannel.open(); server.bind(new InetSocketAddress("0.0.0.0", 8080)); // 创建一个多路复用器 Selector selector = Selector.open(); // 将ServerSocketChannel注册到多路复用器上,并声明关注其ACCEPT就绪事件 server.register(selector, SelectionKey.OP_ACCEPT); while (selector.select() != 0) { // 遍历所有就绪的Channel关联的SelectionKey Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); // 如果这个Channel是READ就绪 if (key.isReadable()) { // 读取该Channel ((SocketChannel) key.channel()).read(ByteBuffer.allocate(10)); } if (key.isWritable()) { //... ... } // 如果这个Channel是ACCEPT就绪 if (key.isAcceptable()) { // 接收新的客户端连接 SocketChannel accept = ((ServerSocketChannel) key.channel()).accept(); // 将新的Channel注册到多路复用器上,并声明关注其READ/WRITE就绪事件 accept.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); } // 删除已经处理过的SelectionKey iterator.remove(); } }
IO多路复用API可以实现用一个线程,去监控所有IO连接的IO就绪事件。
第二个问题在上面的代码中其实也得到了“解决”,但是上面的代码是使用监控IO就绪事件的线程来完成IO的具体操作,如果IO操作耗时较大(比如读操作就绪后,有大量数据需要读取),那么会导致监控线程长时间为某个具体的IO服务,从而导致整个系统长时间无法感知其他IO的就绪事件并分派IO处理任务。所以生产环境中,一般使用一个Boss线程专门用于监控IO就绪事件,一个Work线程池负责具体的IO读写处理。Boss线程检测到新的IO就绪事件后,根据事件类型,完成IO操作任务的分配,并将具体的操作交由Work线程处理。这其实就是Reactor模式的核心思想。
3.2.3 Reactor模式
如上所述,Reactor模式的核心理念在于:
a)依赖于非阻塞IO。
b)使用多路复用器监管海量IO的就绪事件。
c)使用Boss线程和Work线程池分离IO事件的监测与IO事件的处理。
Reactor模式中有如下三类角色:
a)Acceptor。用户处理客户端连接请求。Acceptor角色映射到Java代码中,即为SocketServerChannel。
b)Reactor。用于分派IO就绪事件的处理任务。Reactor角色映射到Java代码中,即为使用多路复用器的Boss线程。
c)Handler。用于处理具体的IO就绪事件。(比如读取并处理数据等)。Handler角色映射到Java代码中,即为Worker线程池中的每个线程。
Acceptor的连接就绪事件,也是交由Reactor监管的,有些地方为了分离连接的建立和对连接的处理,为将Reactor分离为一个主Reactor,专门用户监管连接相关事件(即SelectionKey.OP_ACCEPT),一个从Reactor,专门用户监管连接上的数据相关事件(即SelectionKey.OP_READ 和SelectionKey.OP_WRITE)。
关于Reactor的模型图,网上一搜一大把,我就不献丑了。相信理解了它的核心思想,图自然在心中。关于Reactor模式的应用,可以参见著名NIO编程框架Netty,其实有了Netty之后,一般都直接使用Netty框架进行服务端NIO编程。
3.3 AIO的更进一步
3.3.1 AIO得天独厚的优势
你很容易发现,如果使用AIO,NIO突破时所面临的落地问题天然就不存在了(2个方面阐述AIO与NIO区别)。因为(1)每一个IO操作都可以注册回调函数,天然就不需要专门有一个多路复用器去监听IO就绪事件,也不需要一个Boss线程去分配事件,所有IO操作只要一完成,就天然会通过回调进入自己的下一步处理。
而且,(2)通过AIO,连NIO中Work线程去读写数据的操作都可以省略了,因为AIO是保证数据真正读取/写入完成后,才触发回调函数,用户都不必关注IO操作本身,只需关注拿到IO中的数据后,应该进行的业务逻辑。
简而言之,NIO的多路复用器,是通知你IO就绪事件,AIO的回调是通知你IO完成事件。AIO做的更加彻底一些。这样在某些平台上也会带来性能上的提升,因为AIO的IO读写操作可以交由操作系统内核完成,充分发挥内核潜能,减少了IO系统调用时用户态与内核态间的上下文转换,效率更高。
(不过遗憾的是,Linux内核的AIO实现有很多问题(不在本文讨论范畴),性能在某些场景下还不如NIO,连Linux上的Java都是用epoll来模拟AIO,所以Linux上使用Java的AIO API,只是能体验到异步IO的编程风格,但并不会比NIO高效。综上,Linux平台上的Java服务端编程,目前主流依然采用NIO模型。)
使用AIO API典型编程模式如下:
//创建一个Group,类似于一个线程池,用于处理IO完成事件 AsynchronousChannelGroup group = AsynchronousChannelGroup.withCachedThreadPool(Executors.newCachedThreadPool(), 32); //开启一个AsynchronousServerSocketChannel,在8080端口上监听 AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(group); server.bind(new InetSocketAddress("0.0.0.0", 8080)); //接收到新连接 server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() { //新连接就绪事件的处理函数 @Override public void completed(AsynchronousSocketChannel result, Object attachment) { result.read(ByteBuffer.allocate(4), attachment, new CompletionHandler<Integer, Object>() { //读取完成事件的处理函数 @Override public void completed(Integer result, Object attachment) { } @Override public void failed(Throwable exc, Object attachment) { } }); } @Override public void failed(Throwable exc, Object attachment) { } });
3.3.2 Proactor模式
Java的AIO API其实就是Proactor模式的应用。
也Reactor模式类似,Proactor模式也可以抽象出三类角色:
a)Acceptor。用户处理客户端连接请求。Acceptor角色映射到Java代码中,即为AsynchronousServerSocketChannel。
b)Proactor。用于分派IO完成事件的处理任务。Proactor角色映射到Java代码中,即为API方法中添加回调参数。
c)Handler。用于处理具体的IO完成事件。(比如处理读取到的数据等)。Handler角色映射到Java代码中,即为AsynchronousChannelGroup 中的每个线程。
可见,Proactor与Reactor最大的区别在于:
a)无需使用多路复用器。
b)Handler无需执行具体的IO操作(比如读取数据或写入数据),而是只执行IO数据的业务处理。
http://www.cnblogs.com/itZhy/p/7727569.html