4、EventLoop与EventLoopGroup:事件循环机制,线程模型详解
好,咱们今天来聊聊 Netty 里最核心的概念之一——EventLoop 和 EventLoopGroup。
说实话,我刚接触 Netty 那会儿,也被这两个东西绕得有点晕。什么事件循环、线程模型、单线程多线程……听起来挺唬人的。但说白了,它们就是 Netty 用来处理网络事件的「工人」和「班组」。
你想想看,一个服务端要处理成千上万个连接,每个连接都在收发数据。谁来干这些活?怎么分配?这就是 EventLoop 要解决的问题。
4.1 什么是 EventLoop?
EventLoop,直译过来就是「事件循环」。它的核心思想很简单:一个线程,在一个循环里,不停地处理事件。
我习惯把它理解成一个「永动机」——启动之后就不停地转,从任务队列里拿任务执行,处理 IO 事件,执行定时任务……直到你让它停下来。
核心要点:
- 一个 EventLoop 绑定一个固定的线程(Thread)
- 这个线程负责处理所有注册到该 EventLoop 上的 Channel 的 IO 事件
- EventLoop 内部维护了一个任务队列,可以执行普通任务和定时任务
我在项目中遇到过一个问题:有个同事把耗时的业务逻辑直接写在 ChannelHandler 里,结果导致整个 EventLoop 卡住了,其他连接也跟着遭殃。嗯,这里要注意——千万别在 EventLoop 里做阻塞操作,否则你就是在坑自己。
4.2 EventLoopGroup:线程池的另一种形态
EventLoopGroup 可以理解成 EventLoop 的「管理容器」。它负责创建、管理和分配 EventLoop。
说白了,它就是 Netty 里的线程池。但跟 JDK 的 ThreadPoolExecutor 不太一样——EventLoopGroup 有自己的一套分配策略。
| 特性 | EventLoopGroup | ThreadPoolExecutor |
|---|---|---|
| 线程绑定 | 一个 EventLoop 绑定一个线程,生命周期内不变 | 线程可复用,不固定绑定 |
| 任务类型 | IO 事件 + 普通任务 + 定时任务 | 普通任务 |
| 分配策略 | 轮询或哈希,保证同一个 Channel 绑定同一个 EventLoop | 抢占式分配 |
为什么要保证同一个 Channel 绑定同一个 EventLoop?
因为无锁化。你想想看,如果一个 Channel 的读写被多个线程操作,那就得加锁,加锁就有性能损耗。Netty 的设计哲学是:一个 Channel 的所有操作都在同一个线程里完成,这样就不需要锁了。高,实在是高。
4.3 三种线程模型
Netty 支持三种线程模型,我一个个来讲。
4.3.1 单线程模型
一个 EventLoopGroup 里只有一个 EventLoop。所有 Channel 都注册到这个 EventLoop 上。
EventLoopGroup group = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(group);
这种模型适合小规模、低并发的场景。比如你写个简单的 Demo,或者内部工具。
我曾经在一个内部监控系统里用过单线程模型,连接数不超过 100,跑得挺稳。但你要是拿它去抗几万连接……嗯,那画面太美我不敢看。
警告:单线程模型下,如果一个 Channel 的处理逻辑阻塞了,所有其他 Channel 都得等着。这就是所谓的「队头阻塞」问题。
4.3.2 多线程模型
一个 EventLoopGroup 里有多个 EventLoop。所有 Channel 被均匀分配到这些 EventLoop 上。
EventLoopGroup group = new NioEventLoopGroup(4); // 4 个线程
ServerBootstrap b = new ServerBootstrap();
b.group(group);
这是最常用的模型。我一般会根据 CPU 核心数来设置线程数,通常是 2 * CPU 核心数。
为什么是这个数?因为 Netty 的 EventLoop 主要处理 IO 事件,IO 操作通常不会占满 CPU,所以可以稍微多开几个线程。
小技巧:如果你不确定该设多少个线程,可以用 NettyRuntime.availableProcessors() * 2 这个公式。Netty 自己也是这么算的。
4.3.3 主从多线程模型
这个模型有两个 EventLoopGroup:
- Boss Group:负责接受连接(Acceptor)
- Worker Group:负责处理读写事件(IO Worker)
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(4);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
这是生产环境里最推荐的模型。Boss 线程只做一件事——接受新连接,然后把连接注册到 Worker 线程上。
我做过一个网关项目,压测到 10 万并发连接,用的就是主从多线程模型。Boss 设了 2 个线程,Worker 设了 8 个线程,跑得很稳。
为什么主从模型更好?
- 职责分离:接受连接和处理读写互不干扰
- 性能更好:Boss 线程可以快速响应新连接,不会被已有连接的读写拖累
- 扩展性强:可以根据需要分别调整 Boss 和 Worker 的线程数
4.4 线程模型的底层原理
咱们来扒一扒底层实现。Netty 的 EventLoop 底层用的是 Selector(NIO 的多路复用器)。
每个 EventLoop 内部都有一个 Selector,它负责监听注册到该 EventLoop 上的所有 Channel 的 IO 事件。
伪代码大概是这样的:
while (!isShutdown) {
// 1. 处理 IO 事件
selector.select(timeout);
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
// 处理读写事件
processSelectedKey(key);
}
// 2. 处理普通任务
runAllTasks();
// 3. 处理定时任务
runScheduledTasks();
}
看到了吗?这就是「事件循环」的真面目——一个死循环,不停地做三件事:处理 IO、执行任务、跑定时器。
我个人习惯把这个循环叫做「三合一引擎」。它把 IO 处理、业务逻辑、定时任务都揉在一起了,但通过不同的队列来隔离,互不干扰。
4.5 避坑指南
最后,分享几个我踩过的坑:
- 不要在 EventLoop 里做耗时操作——我曾经在 handler 里写了个数据库查询,结果压测时整个服务卡死了。后来改成异步处理,问题解决。
- 注意 EventLoop 的线程数——设得太少,CPU 利用率上不去;设得太多,线程切换开销大。我一般用
2 * CPU 核心数作为起点。 - 主从模型的 Boss 线程数不要太多——接受连接这个操作本身不重,1 到 2 个线程就够用了。设多了反而浪费。
- 小心阻塞操作——EventLoop 里不要用
Thread.sleep()、Object.wait()这类阻塞方法。如果你非要用,请用 Netty 提供的schedule()方法。
好了,EventLoop 和线程模型就讲到这里。下一节咱们聊聊 Channel 和 Pipeline,看看数据是怎么在 Netty 里流转的。