【NIO】8. Selector多线程

Selector多线程

  • selector多线程

  • 第一版

  • 第二版

  • 第三版

  • 单线程可以配合selector,完成对多个Channel的可读写事件的监控。

    如果是多核CPU,仅仅使用单线程的话,就无法充分利用多核CPU的优势。

  • 另外,只使用单线程,如果某个channel上,处理事件的耗时太长,也会影响其他channel上事件的处理。

selector多线程

一个线程对应一个selector;

一个(线程+selector)处理accept事件,建立连接;

其他(线程+selector)处理其他数据读写事件;

第一版

服务器端主线程:

java 复制代码
public static void main(String[] args) throws IOException {

        Thread.currentThread().setName("Boss");
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(9527));
        serverSocketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        // 创建固定数量的worker
        Worker worker = new Worker("worker-0");
        worker.init();

        while (true) {
            selector.select();

            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                // 主线程只负责处理 accept事件
                if (key.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    log.info("客户端已连接:" + socketChannel);


                    log.info("客户端channel注册到worker线程的 selector-开始");
                    // 连接建立后,将channel注册到worker线程的 selector上, 并且监听read事件;
                    // 【这行代码运行在boss线程】
                    socketChannel.register(worker.selector, SelectionKey.OP_READ, null);
                    log.info("客户端channel注册到worker线程的 selector-结束");

                }
            }
        }

    }

worker线程:

java 复制代码
// worker线程负责处理 读写事件
    static class Worker implements Runnable {

        // worker线程,应该有自己的Selector
        private Selector selector;
        private Thread thread;

        private String name;
        private volatile boolean start;

        public Worker(String name) {
            this.name = name;
        }


        // 初始化线程和selector
        public void init() throws IOException {
            // 初始化只做一遍
            if (!start) {
                thread = new Thread(this, name);
                selector = Selector.open();
                thread.start();

                start = true;
            }
        }

        @Override
        public void run() {

            // worker线程,负责处理 读写事件
            while (true) {
                try {
                    // 【该行代码运行在worker线程】
                    selector.select();

                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey selectionKey = iterator.next();
                        iterator.remove();
                        if (selectionKey.isReadable()) {
                            ByteBuffer byteBuffer = ByteBuffer.allocate(16);
                            SocketChannel channel = (SocketChannel) selectionKey.channel();
                            channel.read(byteBuffer);

                            byteBuffer.flip();

                            CharBuffer decode = Charset.defaultCharset().decode(byteBuffer);
                            log.info("收到客户端数据:" + decode);
                            byteBuffer.clear();
                        }
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }
    }

客户端:

客户端连接到服务器后,立即发送数据;

先触发了服务器连接事件,再出发了read事件。

java 复制代码
public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();

        socketChannel.connect(new InetSocketAddress("localhost",9527));

        socketChannel.write(Charset.defaultCharset().encode("1234567890abcdef"));

        System.in.read();
    }

存在问题:

服务器端打印输出:

text 复制代码
"C:\Program Files\Java\jdk-1.8\bin\java.exe" ...
Connected to the target VM, address: '127.0.0.1:63211', transport: 'socket'
22:29:39 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:63544]
22:29:39 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始

服务器端和客户端分别启动后,可以看到客户端成功连接到服务器;

但是客户端channel,没有注册到worker线程的 selector,服务器也没有收到客户端的数据,没有触发read事件;

问题原因:

  • 服务器端,创建好worker线程后,就直接初始化了,worker线程启动后,调用了selector的select方法,阻塞住了worker线程;
  • 而worker线程的 selector阻塞住之后,就会影响客户端channel的注册;

因此,解决问题的关键,就是【先将客户端channel注册到worker线程的selector上,再调用worker的selector的select方法】。

【客户端channel注册到worker线程的selector上】的逻辑,运行在主线程中;

【调用worker的selector的select方法】的逻辑,运行在worker线程中;

以上两段逻辑的先后顺序如何保证。

思考以下方案:

将worker线程的初始化,和【客户端channel注册到worker线程的selector】,这两个逻辑放在一起,是否可行?

java 复制代码
log.info("客户端channel注册到worker线程的 selector-开始");

worker.init();
socketChannel.register(worker.selector, SelectionKey.OP_READ, null);
log.info("客户端channel注册到worker线程的 selector-结束");

根据结果来看,似乎可行,但是这种方式,只是碰巧,客户端channel注册到worker线程的selector,然后再调用了select方法,不能真正的保证顺序;

而且,此时如果再来个客户端,此时selector正阻塞在select方法,一定会影响新客户端channel的注册。

text 复制代码
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:64486]
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-结束
22:53:47 [INFO ] [worker-0] c.w.p.SelectorMultiThread - 收到客户端数据:1234567890abcdef

新启动一个客户端,可以看到新客户端也成功连接到服务器,但是没有注册到worker线程的selector上。

text 复制代码
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:64486]
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始
22:53:47 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-结束
22:53:47 [INFO ] [worker-0] c.w.p.SelectorMultiThread - 收到客户端数据:1234567890abcdef
23:06:07 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:64728]
23:06:07 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始

第二版

在第一版中

【客户端channel注册到worker线程的selector上】的逻辑,运行在主线程中;

【调用worker的selector的select方法】的逻辑,运行在worker线程中;

如果想办法让这两段逻辑运行在同一个线程中,就可以保证执行的先后顺序。

主线程:

java 复制代码
public static void main(String[] args) throws IOException {

        Thread.currentThread().setName("Boss");
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(9527));
        serverSocketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        // 创建固定数量的worker
        Worker worker = new Worker("worker-0");


        while (true) {
            selector.select();

            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                // 主线程只负责处理 accept事件
                if (key.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    log.info("客户端已连接:" + socketChannel);

                    // 初始化worker

                    log.info("客户端channel注册到worker线程的 selector-开始");
                    // 连接建立后,将channel注册到worker线程的 selector上, 并且监听read事件;
                    worker.init(socketChannel);
                    log.info("客户端channel注册到worker线程的 selector-结束");
                }
            }
        }

    }

worker线程:

java 复制代码
static class Worker implements Runnable {
        private Thread thread;
        private Selector selector;
        private String name;
        private volatile boolean start = false;
        
        private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();


        public Worker(String name) {
            this.name = name;
        }

        // 初始化【线程】和【selector】
        public void init(SocketChannel socketChannel) throws IOException {
            if (!start) {
                this.thread = new Thread(this, name);
                selector = Selector.open();
                thread.start();
                start = true;
            }

            // 向队列中添加任务,但是任务没有立刻执行
            queue.add(() -> {
                try {
                    socketChannel.register(selector, SelectionKey.OP_READ, null);
                } catch (ClosedChannelException e) {
                    throw new RuntimeException(e);
                }
            });

            // 将 selector 从阻塞中唤醒
            selector.wakeup();
        }

        @Override
        public void run() {
            while (true) {
                try {
                    selector.select();
                    Runnable runnable = queue.poll();
                    if (runnable != null) {
                        runnable.run();
                    }
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        iterator.remove();
                        if (key.isReadable()) {
                            ByteBuffer byteBuffer = ByteBuffer.allocate(16);
                            SocketChannel channel = (SocketChannel) key.channel();
                            channel.read(byteBuffer);

                            byteBuffer.flip();

                            CharBuffer decode = Charset.defaultCharset().decode(byteBuffer);
                            log.info(String.valueOf(decode));
                            byteBuffer.clear();
                        }
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

启动多个客户端,可以看到正常连接,并且正常接收到数据

text 复制代码
23:23:52 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:65125]
23:23:52 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始
23:23:52 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-结束
23:23:52 [INFO ] [worker-0] c.w.p.SelectorMultiThread - 1234567890abcdef
23:24:03 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端已连接:java.nio.channels.SocketChannel[connected local=/127.0.0.1:9527 remote=/127.0.0.1:65137]
23:24:03 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-开始
23:24:03 [INFO ] [Boss] c.w.p.SelectorMultiThread - 客户端channel注册到worker线程的 selector-结束
23:24:03 [INFO ] [worker-0] c.w.p.SelectorMultiThread - 1234567890abcdef

关键点:

  1. 使用队列,将【客户端channel注册到selector】的逻辑先保存起来;每次有客户端连接,都要添加这段逻辑;
  2. 在没有读写事件发生时,worker的selector一定是阻塞的,下次再有客户端连接,一定会注册失败,因此,要通过selector的wakewp方法,唤醒selector;(除了事件可以唤醒selector,也可以手动调用wakeup)
  3. selector被唤醒后,如果队列中有任务,说明有客户端channel需要注册;
  4. 客户端channel注册完成后,再处理读写事件;

第三版

服务器端创建,多个 worker,数量为CPU核心数;

Runtime.getRuntime().availableProcessors()

这个方法有坑,如果在Docker环境,拿到的是物理机的核心数,不是容器的核心数。

java 复制代码
public static void main(String[] args) throws IOException {

        Thread.currentThread().setName("Boss");
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(9527));
        serverSocketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);
        
        // 创建固定数量的worker
        Worker[] workers = new Worker[Runtime.getRuntime().availableProcessors()];
        for (int i = 0; i < workers.length; i++) {
            workers[i] = new Worker("worker-" + i);
        }

        AtomicInteger count = new AtomicInteger();


        while (true) {
            selector.select();

            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                // 主线程只负责处理 accept事件
                if (key.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    log.info("客户端已连接:" + socketChannel);

                    // 初始化worker

                    log.info("客户端channel注册到worker线程的 selector-开始");
                    // 连接建立后,将channel注册到worker线程的 selector上, 并且监听read事件;
                    workers[count.getAndIncrement() % workers.length].init(socketChannel);
                    log.info("客户端channel注册到worker线程的 selector-结束");
                }
            }
        }

    }

worker:

java 复制代码
static class Worker implements Runnable {
        private Thread thread;
        private Selector selector;
        private String name;
        private volatile boolean start = false;
        private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();


        public Worker(String name) {
            this.name = name;
        }

        // 初始化【线程】和【selector】
        public void init(SocketChannel socketChannel) throws IOException {
            if (!start) {
                this.thread = new Thread(this, name);
                selector = Selector.open();
                thread.start();
                start = true;
            }

            // 向队列中添加任务,但是任务没有立刻执行
            queue.add(() -> {
                try {
                    socketChannel.register(selector, SelectionKey.OP_READ, null);
                } catch (ClosedChannelException e) {
                    throw new RuntimeException(e);
                }
            });

            // 将 selector 从阻塞中唤醒
            selector.wakeup();
        }

        @Override
        public void run() {
            while (true) {
                try {
                    selector.select();
                    Runnable runnable = queue.poll();
                    if (runnable != null) {
                        runnable.run();
                    }
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        iterator.remove();
                        if (key.isReadable()) {
                            ByteBuffer byteBuffer = ByteBuffer.allocate(16);
                            SocketChannel channel = (SocketChannel) key.channel();
                            channel.read(byteBuffer);

                            byteBuffer.flip();

                            CharBuffer decode = Charset.defaultCharset().decode(byteBuffer);
                            log.info(String.valueOf(decode));
                            byteBuffer.clear();
                        }
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
相关推荐
蚰蜒螟5 天前
从内核启动到 Java NIO:一次 socket accept 的源码串联之旅
java·python·nio
骇客野人7 天前
Java BIO / NIO / AIO 完整详解 + 编程技巧
java·开发语言·nio
那个松鼠很眼熟w12 天前
【NIO】4. ByteBuffer的几个重要方法
nio
那个松鼠很眼熟w12 天前
【NIO】3.1 ByteBuffer的原理
nio
那个松鼠很眼熟w12 天前
【NIO】3.2 ByteBuffer的原理-AI参考
nio
那个松鼠很眼熟w12 天前
【NIO】5. FileChannel的基本用法
nio
那个松鼠很眼熟w12 天前
【NIO】2. ByteBuffer的基本使用
nio
今天的砖头有点烫手啊17 天前
Java IO/NIO/AIO 演进:从 BIO 到 Netty 的底层逻辑
java·jvm·nio
名字还没想好☜1 个月前
Java NIO ByteBuffer 实战:flip/clear/compact 三个绕晕人的方法与 position/limit 心智模型
java·开发语言·后端·spring·nio