BIO-NIO与多路复用

本文深入剖析BIO、NIO与多路复用(select/poll/epoll)三种IO模型的本质差异,聚焦于‘等待主体’的演进:从线程阻塞等待,到用户态轮询,再到内核代为监控就绪事件。重点对比其时间复杂度、资源开销及适用场景,并指出Netty基于epoll的Reactor封装如何支撑高并发网络服务,为数据库连接池、网关、消息队列等中间件提供底层基础。

BIO、NIO 与多路复用

一个服务要扛一万个连接。BIO 的写法是一个连接一个线程,一万个线程,每个都在干等数据来。光是线程栈就吃掉几个 GB,CPU 忙着在它们之间倒手,业务逻辑没跑几行。NIO 和多路复用解决的就是这笔账,演进的主线只有一个问题:等待这件事,谁来干。

BIO:线程干等

BIO 的 accept、read、write 全是阻塞的。线程调 read 就挂在那,数据不来不动窝,来了才醒。模型简单直观,代价是「一个连接一个线程」,线程数跟着连接数走。连接大部分是闲的,线程大部分在睡,睡也是要占内存、要调度的。

NIO:自己轮询

Java NIO 给了三件套,Channel(通道)、Buffer(缓冲区)、Selector(选择器)。连接设成非阻塞之后,read 没数据立刻返回,线程可以去问下一个连接。Selector 就是「一次问一圈」的工具,一个线程管理成千上万个 Channel,谁就绪了处理谁。
账变了,线程数不再跟连接数走。但 Selector 底下的实现在操作系统层面有个演进过程,这决定了「问一圈」的效率。

多路复用:让内核代看

select 和 poll 是第一代。每次调用都把整个连接列表从用户态拷给内核,内核挨个看一遍谁就绪,返回后程序再挨个找。一万连接看一圈是 O(n),大部分连接还是没数据,白看。
epoll 是第二代。连接注册一次,之后内核只把「就绪的」推给你,看几个拿几个,O(活跃数)。连接再多,活跃的就那么些,账就平了。

epoll

连接注册一次

内核只推就绪的

拿几个处理几个

select / poll 每轮

拷全量连接表进内核

内核挨个问:你有数据吗

返回后程序再挨个筛

Netty 站在这层肩膀上。它把 Reactor 模式(事件来了分发给处理者)封装好,加上线程模型、缓冲区管理和协议编解码,让你写业务而不是写 Selector 循环。你在用的中间件,消息队列客户端、网关、SSE 流式输出,底下大都是这条路。

亲手看账单

下面用一段 Java 起一个单线程的 Selector 服务,一百个客户端连接各发一条消息,看一个线程怎么扛下全部读写。直接运行 java IoBill.java(JDK 11+)。

// 直接运行:java IoBill.java
// 一个 Selector 线程扛 100 个连接,对照 BIO 的一线程一连接。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicInteger;

public class IoBill {

    public static void main(String[] args) throws Exception {
        Selector selector = Selector.open();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress("127.0.0.1", 0));
        ssc.configureBlocking(false);
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        int port = ((InetSocketAddress) ssc.getLocalAddress()).getPort();
        AtomicInteger handled = new AtomicInteger();

        // 唯一的 reactor 线程:accept + read + echo
        Thread reactor = new Thread(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    selector.select(100);
                    Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                    while (it.hasNext()) {
                        SelectionKey key = it.next();
                        it.remove();
                        if (key.isAcceptable()) {
                            SocketChannel ch = ((ServerSocketChannel) key.channel()).accept();
                            ch.configureBlocking(false);
                            ch.register(selector, SelectionKey.OP_READ);
                        } else if (key.isReadable()) {
                            SocketChannel ch = (SocketChannel) key.channel();
                            ByteBuffer buf = ByteBuffer.allocate(64);
                            if (ch.read(buf) > 0) {
                                buf.flip();
                                ch.write(buf);
                                handled.incrementAndGet();
                            } else {
                                ch.close();
                            }
                        }
                    }
                }
            } catch (IOException e) {
                // 关闭时退出
            }
        }, "single-reactor");
        reactor.start();

        // 100 个客户端,各发一条消息并等 echo
        Thread[] clients = new Thread[100];
        for (int i = 0; i < clients.length; i++) {
            clients[i] = new Thread(() -> {
                try (SocketChannel ch = SocketChannel.open(new InetSocketAddress("127.0.0.1", port))) {
                    ch.write(ByteBuffer.wrap("ping".getBytes()));
                    ByteBuffer buf = ByteBuffer.allocate(8);
                    ch.read(buf);
                } catch (IOException e) {
                    // 演示不处理
                }
            });
            clients[i].start();
        }
        for (Thread t : clients) {
            t.join();
        }

        System.out.println("单一 reactor 线程处理了 " + handled.get() + " 次读写");
        System.out.println("BIO 对照:同样 100 个连接要 100 个阻塞线程,栈内存与切换开销乘 100。");
        reactor.interrupt();
        selector.wakeup();
    }
}

记住一句话:IO 模型的演进,是「谁在等」的演进,从线程干等,到程序自己看,再到内核代看。

适用边界

这一篇管 IO 模型的账。epoll 的惊群和 LT/ET 触发模式、Netty 的线程模型细节,以及虚拟线程对 BIO 的复兴(JDK 21 之后写阻塞代码也能高并发),是另外的题。演示用的是单线程 Selector,真实服务是 Reactor 主从多线程,账的量级不变。跑完演示再想那句「一万个连接」,选型时就知道自己的服务站在哪一层。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值