【1】BIO,NIO,AIO与Reactor,Proactor

讲解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

上一篇:EF通用数据层封装类(支持读写分离,一主多从)


下一篇:java与javac版本不一致问题