Java并发编程学习-日记3、Reactor反应器模式

        本博文记录了学习Reactor反应器模式的一些笔记。java.util.concurrent包的作者,大师Doug Lea关于分析与构建可伸缩的高性能IO服务的一篇经典文章——《Scalable IO in Java》,在文章中Doug Lea通过各个角度,循序渐进的梳理了服务开发中的相关问题,以及在解决问题的过程中服务模型的演变与进化,文章中基于Reactor反应器模式的几种服务模型架构,也被Netty、Mina等大多数高性能IO服务框架所采用。所以可以仔细拼读一下《Scalable IO in Java》

       反应器模式由Reactor反应器线程、Handlers处理器两大角色组成: (1)Reactor反应器线程的职责:负责响应IO事件,并且分发到Handlers处理器。这里的IO事件,就是NIO中选择器监控的通道IO事件。(2)Handlers处理器的职责:非阻塞的执行业务处理逻辑。与IO事件(或者选择键)绑定,负责IO事件的处理。完成真正的连接建立、通道的读取、处理业务逻辑、负责将结果写出到通道等。

1、单线程Reactor反应器

class EchoServerReactor implements Runnable {
    Selector selector;
    ServerSocketChannel serverSocket;

    EchoServerReactor() throws IOException {
        selector = Selector.open();                                       //Reactor初始化
        serverSocket = ServerSocketChannel.open();

        InetSocketAddress address = new InetSocketAddress(127.0.0.1,9000);
        serverSocket.socket().bind(address);
        serverSocket.configureBlocking(false);                   //非阻塞
       
SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT); //分步处理,第一步,接收accept事件

       //将新连接处理器作为附件绑定到sk选择键上,在选择键注册完成之后,调用attach方法,将Handler处理器绑定到选择键;
        sk.attach(new AcceptorHandler());    
    }

    //轮询监听
    public void run() {
        try {
            while (!Thread.interrupted()) {
                selector.select();
                Set<SelectionKey> selected = selector.selectedKeys();
                 Iterator<SelectionKey> it = selected.iterator();
                while (it.hasNext()) {
                    //Reactor负责dispatch收到的事件
                    SelectionKey sk = it.next();
                    dispatch(sk);
                }
                selected.clear();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    void dispatch(SelectionKey sk) {

      //当事件发生时,调用attachment方法,可以从选择键取出Handler处理器,将事件分发到Handler处理器中,完成业务处理。( 调用之前attach绑定到选择键的handler处理器对象,attachment与 attach往往是结合使用。)
        Runnable handler = (Runnable) sk.attachment();
        if (handler != null) {
            handler.run();
        }
    }

    /**

       *Handler:新连接处理器

       *接收新连接,并创建一个输入输出处理器。
    class AcceptorHandler implements Runnable {
        public void run() {
            try {
                SocketChannel channel = serverSocket.accept();
                if (channel != null)
                    new EchoHandler(selector, channel);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws IOException {
        new Thread(new EchoServerReactor()).start();
    }
}

 

 

class EchoHandler implements Runnable {
    final SocketChannel channel;
    final SelectionKey sk;
    final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    static final int RECIEVING = 0, SENDING = 1;
    int state = RECIEVING;

    EchoHandler(Selector selector, SocketChannel c) throws IOException {
        channel = c;
        c.configureBlocking(false);
        //仅仅取得选择键
        sk = channel.register(selector, 0);

        //将Handler作为选择键的附件
        sk.attach(this);

        //第二步,注册Read就绪事件
        sk.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    public void run() {

        try {

            if (state == SENDING) {
                //写入通道
                channel.write(byteBuffer);
                //写完后,准备开始从通道读,byteBuffer切换成写模式
                byteBuffer.clear();
                //写完后,注册read就绪事件
                sk.interestOps(SelectionKey.OP_READ);
                //写完后,进入接收的状态
                state = RECIEVING;
            } else if (state == RECIEVING) {
                //从通道读
                int length = 0;
                while ((length = channel.read(byteBuffer)) > 0) {
                    Logger.info(new String(byteBuffer.array(), 0, length));
                }
                //读完后,准备开始写入通道,byteBuffer切换成读模式
                byteBuffer.flip();
                //读完后,注册write就绪事件
                sk.interestOps(SelectionKey.OP_WRITE);
                //读完后,进入发送的状态
                state = SENDING;
            }
            //处理结束了, 这里不能关闭select key,需要重复使用
            //sk.cancel();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

在以上单线程反应器模式中,Reactor反应器和Handler处理器,都执行在同一条线程上。这样,带来了一个问题:当其中某个Handler阻塞时,会导致其他所有的Handler都得不到执行。另外,目前的服务器都是多核的,单线程反应器模式模型不能充分利用多核资源。

2、多线程池Reactor反应器

基于对单线程反应器模式的分析,多线程反应器可以分为两个方面:(1)、处理器多线程;分离业务处理线程和服务监听及IO事件查询线程。(2)、反应器多线程;引入多个选择器,每个子反应器负责一个选择器,以充分利用CPU。

public class MultiThreadEchoServerReactor {

    ServerSocketChannel serverSocket;

    AtomicInteger next = new AtomicInteger(0);

    Selector[] selectors = new Selector[2]; //selectors集合,引入多个selector选择器

    SubReactor[] subReactors = null; //引入多个子反应器

   public MultiThreadEchoServerReactor() throws IOException {

        //初始化多个selector选择器

        selectors[0] = Selector.open();

        selectors[1] = Selector.open();

        serverSocket = ServerSocketChannel.open();

        serverSocket.socket().bind(new InetSocketAddress(127.0.0.1, 9000));

        serverSocket.configureBlocking(false); //非阻塞

        //第一个selector,负责监控新连接事件

        SelectionKey sk = serverSocket.register(selectors[0], SelectionKey.OP_ACCEPT);

        //附加新连接处理handler处理器到SelectionKey(选择键)

        sk.attach(new AcceptorHandler());

        //第一个子反应器负责第一个选择器

        SubReactor subReactor1 = new SubReactor(selectors[0]);

        //第二个子反应器负责第二个选择器

        SubReactor subReactor2 = new SubReactor(selectors[1]);

        subReactors = new SubReactor[]{subReactor1, subReactor2};

    }

    private void startService() {

        // 一子反应器对应一条线程

        new Thread(subReactors[0]).start();

        new Thread(subReactors[1]).start();

    }

    //反应器

    class SubReactor implements Runnable {

        //每条线程负责一个选择器的查询

        final Selector selector;

        public SubReactor(Selector selector) {

            this.selector = selector;

        }

        public void run() {

            try {

                while (!Thread.interrupted()) {

                    selector.select();

                    Set<SelectionKey> keySet = selector.selectedKeys();

                    Iterator<SelectionKey> it = keySet.iterator();

                    while (it.hasNext()) {

                        //Reactor负责dispatch收到的事件

                        SelectionKey sk = it.next();

                        dispatch(sk);

                    }

                    keySet.clear();

                }

            } catch (IOException ex) {

                ex.printStackTrace();

            }

        }

        void dispatch(SelectionKey sk) {

            Runnable handler = (Runnable) sk.attachment();

            //调用之前attach绑定到选择键的handler处理器对象

            if (handler != null) {

                handler.run();

            }

        }

    }

    // Handler:新连接处理器

    class AcceptorHandler implements Runnable {

        public void run() {

            try {

                SocketChannel channel = serverSocket.accept();

                if (channel != null)

                    new MultiThreadEchoHandler(selectors[next.get()], channel);

            } catch (IOException e) {

                e.printStackTrace();

            }

            if (next.incrementAndGet() == selectors.length) {

                next.set(0);

            }

        }

    }

    public static void main(String[] args) throws IOException {

        MultiThreadEchoServerReactor server = new MultiThreadEchoServerReactor();

        server.startService();

    }

}

//多线程处理器

public class MultiThreadEchoHandler implements Runnable {

    final SocketChannel channel;

    final SelectionKey sk;

    final ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

    static final int RECIEVING = 0, SENDING = 1;

    int state = RECIEVING;

    //引入线程池

    static ExecutorService pool = Executors.newFixedThreadPool(4);

    public MultiThreadEchoHandler(Selector selector, SocketChannel c) throws IOException {

        channel = c;

        c.configureBlocking(false);

        //仅仅取得选择键,后设置感兴趣的IO事件

        sk = channel.register(selector, 0);

        //将本Handler作为sk选择键的附件,方便事件dispatch

        sk.attach(this);

        //sk选择键注册Read就绪事件

        sk.interestOps(SelectionKey.OP_READ);

        selector.wakeup();

    }

    public void run() {

        //异步任务,在独立的线程池中执行

        pool.execute(new AsyncTask());

    }

    //异步任务,不在Reactor线程中执行,使用同步修饰符synchronized,保证统一线程池中的业务处理可以有序进行。

    public synchronized void asyncRun() {

        try {

            if (state == SENDING) {

                //写入通道

                channel.write(byteBuffer);

                //写完后,准备开始从通道读,byteBuffer切换成写模式

                byteBuffer.clear();

                //写完后,注册read就绪事件

                sk.interestOps(SelectionKey.OP_READ);

                //写完后,进入接收的状态

                state = RECIEVING;

            } else if (state == RECIEVING) {

                //从通道读

                int length = 0;

                while ((length = channel.read(byteBuffer)) > 0) {

                    Logger.info(new String(byteBuffer.array(), 0, length));

                }

                //读完后,准备开始写入通道,byteBuffer切换成读模式

                byteBuffer.flip();

                //读完后,注册write就绪事件

                sk.interestOps(SelectionKey.OP_WRITE);

                //读完后,进入发送的状态

                state = SENDING;

            }

            //处理结束了, 这里不能关闭select key,需要重复使用

            //sk.cancel();

        } catch (IOException ex) {

            ex.printStackTrace();

        }

    }

    //异步任务的内部类,实现异步业务asyncRun独立的提交到线程池中。

    class AsyncTask implements Runnable {

        public void run() {

            MultiThreadEchoHandler.this.asyncRun();

        }

    }

}

3、反应器模式和生产者消费者模式对比

  • 相似之处:在一定程度上,反应器模式有点类似生产者消费者模式。在生产者消费者模式中,一个或多个生产者将事件加入到一个队列中,一个或多个消费者主动地从这个队列中提取(Pull)事件来处理。
  • 不同之处在于:反应器模式是基于查询的,没有专门的队列去缓冲存储IO事件,查询到IO事件之后,反应器会根据不同IO选择键(事件)将其分发给对应的Handler处理器来处理。

4、反应器模式和观察者模式(Observer Pattern)对比

  • 相似之处在于:在反应器模式中,当查询到IO事件后,服务处理程序使用单路/多路分发(Dispatch)策略,同步地分发这些IO事件。观察者模式(Observer Pattern)也被称作发布/订阅模式,它定义了一种依赖关系,让多个观察者同时监听某一个主题(Topic)。这个主题对象在状态发生变化时,会通知所有观察者,它们能够执行相应的处理。
  • 不同之处在于:在反应器模式中,Handler处理器实例和IO事件(选择键)的订阅关系,基本上是一个事件绑定到一个Handler处理器;每一个IO事件(选择键)被查询后,反应器会将事件分发给所绑定的Handler处理器;而在观察者模式中,同一个时刻,同一个主题可以被订阅过的多个观察者处理。

5、反应器模式总结

  • 作为高性能的IO模式,反应器模式的优点如下:

  1. 响应快,虽然同一反应器线程本身是同步的,但不会被单个连接的同步IO所阻塞; 编程相对简单,最大程度避免了复杂的多线程同步,也避免了多线程的各个进程之间切换的开销;
  2. 可扩展,可以方便地通过增加反应器线程的个数来充分利用CPU资源。
  • 反应器模式的缺点如下:

  1. 反应器模式增加了一定的复杂性,因而有一定的门槛,并且不易于调试。
  2. 反应器模式需要操作系统底层的IO多路复用的支持,如Linux中的epoll。如果操作系统的底层不支持IO多路复用,反应器模式不会有那么高效。
  3. 同一个Handler业务线程中,如果出现一个长时间的数据读写,会影响这个反应器中其他通道的IO处理。例如在大文件传输时,IO操作就会影响其他客户端(Client)的响应时间。因而对于这种操作,还需要进一步对反应器模式进行改进。
  • Reactor反应器模式事件处理整个流程大致分为4步,具体如下:

第1步:通道注册。IO源于通道(Channel)。IO是和通道(对应于底层连接而言)强相关的。一个IO事件,一定属于某个通道。但是,如果要查询通道的事件,首先要将通道注册到选择器。只需通道提前注册到Selector选择器即可,IO事件会被选择器查询到。

第2步:查询选择。在反应器模式中,一个反应器(或者SubReactor子反应器)会负责一个线程;不断地轮询,查询选择器中的IO事件(选择键)。

第3步:事件分发。如果查询到IO事件,则分发给与IO事件有绑定关系的Handler业务处理器。

第4步:完成真正的IO操作和业务处理,这一步由Handler业务处理器负责。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值