02-Reactor模式与Netty线程模型

发布时间:2026/8/4 3:45:39
02-Reactor模式与Netty线程模型 Netty与网络编程深度解读——Reactor模式与Netty线程模型Netty与网络编程深度解读系列 | 第 2 篇引言上一篇我们拆解了NIO的Buffer、Channel和Selector三大组件,理解了NIO的底层机制。但直接用NIO原生API写服务端,代码冗长、容易出错——需要手动管理Selector轮询、SelectionKey清理、半包处理等细节。Netty通过Reactor模式优雅地封装了这些复杂性。这一篇,我们从Doug Lea的经典论文《Scalable I/O in Java》出发,拆解Reactor三种模型,然后分析Netty的主从Reactor线程模型设计。一、Reactor模式的本质1.1 为什么需要Reactor传统的BIO模型中,每个连接独占一个线程:// BIO服务端while(true){Socketsocket=serverSocket.accept();// 阻塞newThread(()-{while(true){byte[]data=socket.getInputStream().read();// 阻塞// 处理数据}}).start();}最基础的BIO(Blocking I/O)服务端模型。serverSocket.accept()在无连接到达时会阻塞当前线程,直到有客户端连接才会返回Socket对象。随后为每个连接创建一个新线程,在线程内部通过socket.getInputStream().read()阻塞等待数据。这种"一连接一线程"的设计虽然直观,但每个线程默认占用约1MB栈空间,且线程在等待I/O时处于阻塞状态却仍占用系统资源。当连接数从数百增长到上万时,线程数量的膨胀会导致严重的内存浪费和CPU上下文切换开销,这正是Reactor模式要解决的核心痛点。代码中的两个while (true)循环分别代表持续接受新连接和持续读取数据,外层循环保证服务端不断运行,内层循环保证每个连接的线程持续处理该连接的数据流。当连接数增长到万级时,线程爆炸带来三个问题:内存开销:每个线程默认占用1MB栈空间,1万连接=10GB上下文切换:CPU在线程间频繁切换,有效工作时间被压缩线程创建/销毁:频繁GC压力Reactor模式通过事件驱动解决这些问题:不分配线程等待I/O,而是当I/O事件就绪时才分配线程处理。1.2 Reactor的核心思想Reactor模式由三个核心角色组成:角色职责对应NIO组件Reactor监听I/O事件,分发到对应HandlerSelector + 事件循环Acceptor处理新连接接受ServerSocketChannel.accept()Handler处理具体I/O读写和业务逻辑SocketChannel.read()/write()Reactor模式的本质: 事件驱动 ──→ 就绪分发 ──→ 按需处理 Selector监听所有Channel ↓ 有事件就绪 分发到对应的Handler ↓ Handler处理I/O读写二、Reactor三种模型Doug Lea在《Scalable I/O in Java》中描述了Reactor的三种演进形态。2.1 单Reactor单线程最基础的形态:一个线程完成所有工作——监听事件、接受连接、I/O读写、业务处理。┌─────────────────────────────────┐ │ Reactor │ │ (Selector + EventLoop) │ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │Accept│ │Read │ │Write │ │ │ │Handle│ │Handle│ │Handle│ │ │ └──────┘ └──────┘ └──────┘ │ │ Single Thread │ └─────────────────────────────────┘JDK NIO实现:Selectorselector=Selector.open();ServerSocketChannelserverChannel=ServerSocketChannel.open();serverChannel.configureBlocking(false);serverChannel.bind(newInetSocketAddress(8080));serverChannel.register(selector,SelectionKey.OP_ACCEPT);while(true){selector.select();for(SelectionKeykey:selector.selectedKeys()){if(key.isAcceptable()){// Acceptor: 接受新连接SocketChannelclient=serverChannel.accept();client.configureBlocking(false);client.register(selector,SelectionKey.OP_READ);}elseif(key.isReadable()){// Handler: 读取数据 + 业务处理 + 写回响应SocketChannelclient=(SocketChannel)key.channel();ByteBufferbuffer=ByteBuffer.allocate(1024);client.read(buffer);// 业务处理(在同一线程)buffer.flip();client.write(buffer);}}}单Reactor单线程模型的JDK NIO原生实现,完整展示了Reactor模式的核心运转流程。首先通过Selector.open()创建选择器,将ServerSocketChannel注册为非阻塞模式并绑定到8080端口,关注OP_ACCEPT事件。主循环中selector.select()阻塞等待事件就绪,随后遍历selectedKeys()逐一处理。当检测到isAcceptable()时执行Acceptor逻辑——接受新连接并将其注册为OP_READ;当检测到isReadable()时执行Handler逻辑——分配ByteBuffer读取数据、调用flip()翻转缓冲区、写回响应。所有操作都在同一个线程中串行执行,SelectionKey充当了事件类型判断的桥梁。这种设计的核心问题在于:一旦某个Handler的业务处理耗时较长,后续所有连接的事件分发都会被阻塞,因为整个事件循环只有一条执行线程。优点:无需线程同步,实现简单。缺点:一个Handler阻塞会导致所有连接被阻塞。适合I/O少、处理快的场景(如Redis 6.0之前的单线程模型)。2.2 单Reactor