EventLoop 生命周期
整体流程如下:
text
创建 EventLoopGroup
↓
创建多个子 EventLoop,此时通常并不会启动线程
↓
Channel 注册或外部线程提交任务
↓
EventLoop 延迟启动线程
↓
循环处理 I/O 事件、普通任务和定时任务
↓
执行优雅关闭
↓
释放资源并完成
1. EventLoop 的线程模型
理解生命周期之前,需要先明确三个对象之间的关系:
EventLoopGroup:管理一组EventLoop,并负责选择一个EventLoop处理新注册的Channel。EventLoop:本质上是单线程事件执行器,持续处理 I/O 事件、普通任务和定时任务。Channel:注册成功后会绑定到某个EventLoop,后续生命周期事件和 I/O 操作通常都由该线程处理。
EventLoopGroup 可以类比为线程池,但与普通线程池并不完全相同:
- 每个
SingleThreadIoEventLoop同一时刻只有一个线程执行。 - 一个
EventLoop可以被多个Channel注册。 - 一个
Channel注册在EventLoop后便不再发生改变。 - 同一
Channel的事件按顺序执行,因此可以减少并发竞争和锁的使用。
2. 状态机
SingleThreadEventExecutor 使用以下状态描述生命周期:
java
private static final int ST_NOT_STARTED = 1;
private static final int ST_SUSPENDING = 2;
private static final int ST_SUSPENDED = 3;
private static final int ST_STARTED = 4;
private static final int ST_SHUTTING_DOWN = 5;
private static final int ST_SHUTDOWN = 6;
private static final int ST_TERMINATED = 7;
private volatile int state = ST_NOT_STARTED;
状态含义如下:
ST_NOT_STARTED:EventLoop 已创建,但线程尚未启动。ST_SUSPENDING:正在进入暂停状态。ST_SUSPENDED:已暂停,后续可以重新启动。ST_STARTED:线程已经启动,事件循环正在运行。ST_SHUTTING_DOWN:正在优雅关闭,仍可能处理遗留任务。ST_SHUTDOWN:已进入最终关闭阶段。ST_TERMINATED:线程和资源均已终止。
正常情况下,主要状态转换为:
text
ST_NOT_STARTED
↓
ST_STARTED
↓
ST_SHUTTING_DOWN
↓
ST_SHUTDOWN
↓
ST_TERMINATED
支持暂停和恢复时,还可能经过 ST_SUSPENDING 和 ST_SUSPENDED。
3. 创建 EventLoopGroup
以 EchoServer 中的初始化代码为例:
java
EventLoopGroup group =
new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
构造过程最终进入 MultithreadEventExecutorGroup:
java
protected MultithreadEventExecutorGroup(
int nThreads,
Executor executor,
EventExecutorChooserFactory chooserFactory,
Object... args) {
checkPositive(nThreads, "nThreads");
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i++) {
boolean success = false;
try {
children[i] = newChild(executor, args);
success = true;
} finally {
if (!success) {
// 创建失败时,关闭已经创建的子 EventLoop
for (int j = 0; j < i; j++) {
children[j].shutdownGracefully();
}
}
}
}
chooser = chooserFactory.newChooser(children);
FutureListener<Object> terminationListener = future -> {
if (terminatedChildren.incrementAndGet() == children.length) {
terminationFuture.setSuccess(null);
}
};
for (EventExecutor child : children) {
child.terminationFuture().addListener(terminationListener);
}
}
这段代码完成了四件事:
- 创建底层
Executor,用于真正启动EventLoop线程。 - 创建指定数量的子
EventLoop。 - 创建
chooser,用于为新任务或新连接选择EventLoop。 - 监听所有子
EventLoop的终止状态。
此时创建的是 EventLoop 对象,而不是立即启动所有线程。线程采用延迟启动策略,只有注册 Channel 或提交任务时才会真正启动。
每个子 EventLoop 都有一个只读的 terminationFuture。当最后一个子 EventLoop 终止时,EventLoopGroup 的 terminationFuture 才会完成,它表示的是"执行器已经终止"
4. 创建子 EventLoop
子 EventLoop 的主要创建链路如下:
text
MultithreadEventExecutorGroup.newChild
↓
MultiThreadIoEventLoopGroup.newChild
↓
SingleThreadIoEventLoop
↓
SingleThreadEventLoop
↓
SingleThreadEventExecutor
↓
AbstractScheduledEventExecutor
MultiThreadIoEventLoopGroup.newChild 创建具体的 SingleThreadIoEventLoop:
java
protected IoEventLoop newChild(
Executor executor,
IoHandlerFactory ioHandlerFactory,
Object... args) {
return new SingleThreadIoEventLoop(this, executor, ioHandlerFactory);
}
SingleThreadEventExecutor 的构造方法会初始化任务执行所需的核心字段:
java
protected SingleThreadEventExecutor(
EventExecutorGroup parent,
Executor executor,
boolean addTaskWakesUp,
boolean supportSuspension,
int maxPendingTasks,
RejectedExecutionHandler rejectedHandler) {
super(parent);
this.addTaskWakesUp = addTaskWakesUp;
this.supportSuspension = supportSuspension;
this.maxPendingTasks = Math.max(16, maxPendingTasks);
this.executor = ThreadExecutorMap.apply(executor, this);
taskQueue = newTaskQueue(this.maxPendingTasks);
rejectedExecutionHandler =
ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
lastActivityTimeNanos = ticker().nanoTime();
}
关键字段的作用如下:
executor:负责启动 EventLoop 对应的线程。taskQueue:保存普通Runnable任务。maxPendingTasks:限制待执行任务数量,避免队列无限增长。rejectedExecutionHandler:任务队列满或执行器关闭时处理拒绝逻辑。addTaskWakesUp:表示添加任务是否会自动唤醒阻塞中的 EventLoop。supportSuspension:表示是否支持暂停和恢复。lastActivityTimeNanos:记录最近一次任务或 I/O 活动时间。
addTaskWakesUp 为 false 表示任务入队不会隐式唤醒线程;SingleThreadIoEventLoop 会根据任务是否需要立即执行,通过 NioIoHandler.wakeup() 有条件地唤醒 Selector。
5. Channel 注册与任务提交
Channel 注册后会绑定到一个 EventLoop。其 channelRegistered、channelActive、读写事件以及后续的 channelInactive、channelUnregistered 通常都由绑定的 EventLoop 线程执行。
外部线程向 EventLoop 提交任务时,最终会进入 SingleThreadEventExecutor.execute:
java
private void execute(Runnable task, boolean immediate) {
boolean inEventLoop = inEventLoop();
addTask(task);
if (!inEventLoop) {
startThread();
if (isShutdown()) {
boolean reject = false;
try {
if (removeTask(task)) {
reject = true;
}
} catch (UnsupportedOperationException ignore) {
// 某些队列不支持删除
}
if (reject) {
reject();
}
}
}
if (!addTaskWakesUp && immediate) {
wakeup(inEventLoop);
}
}
任务提交流程:
- 判断当前线程是否就是 EventLoop 线程。
- 将任务加入
taskQueue。 - 外部线程提交任务时,确保 EventLoop 已经启动。
- 如果执行器已经关闭,尝试移除并拒绝刚提交的任务。
- 必要时唤醒阻塞在
Selector.select()上的 EventLoop。
公开的 execute() 和 lazyExecute() 最终都会进入上面的内部方法,但唤醒策略不同:
execute(task)根据wakesUpForTask(task)判断是否需要立即唤醒线程。lazyExecute(task)固定以immediate = false提交,适合不要求立即唤醒的低优先级任务。
5.1 Queue 方法的基本方法
任务队列通常使用 offer() 添加任务,而不是使用 add():
offer():队列已满时返回false,便于交给拒绝策略处理。add():队列已满时抛出IllegalStateException。poll():获取并移除队首元素;队列为空时立即返回null。peek():只查看队首元素,不移除;队列为空时返回null。
5.2 I/O 注册必须在 EventLoop 线程执行
SingleThreadIoEventLoop.register() 会根据当前线程决定直接注册,还是把注册动作提交到 EventLoop 的任务队列:
java
public final Future<IoRegistration> register(final IoHandle handle) {
Promise<IoRegistration> promise = newPromise();
if (inEventLoop()) {
registerForIo0(handle, promise);
} else {
execute(() -> registerForIo0(handle, promise));
}
return promise;
}
private void registerForIo0(
final IoHandle handle,
Promise<IoRegistration> promise) {
assert inEventLoop();
try {
IoRegistration registration = ioHandler.register(handle);
numRegistrations.incrementAndGet();
promise.setSuccess(new IoRegistrationWrapper(registration));
} catch (Exception e) {
promise.setFailure(e);
}
}
这段代码说明:
- 真正的
ioHandler.register(handle)始终在 EventLoop 线程执行。 - 外部线程调用注册方法时,返回一个尚未完成的
Promise。 - 注册成功后递增
numRegistrations,并通过 Promise 通知结果。 - 注册失败时调用
promise.setFailure(e),不会直接把异常抛回到外部线程。
6. 延迟启动 EventLoop 线程
startThread 负责将 EventLoop 从未启动或暂停状态切换到运行状态:
java
private void startThread() {
int currentState = state;
if (currentState == ST_NOT_STARTED ||
currentState == ST_SUSPENDED) {
if (STATE_UPDATER.compareAndSet(
this, currentState, ST_STARTED)) {
resetIdleCycles();
resetBusyCycles();
boolean success = false;
try {
doStartThread();
success = true;
} finally {
if (!success) {
STATE_UPDATER.compareAndSet(
this, ST_STARTED, ST_NOT_STARTED);
}
}
}
}
}
这里有三个关键点:
- 使用 CAS 保证并发提交任务时只有一个线程负责启动 EventLoop。
- 启动失败时回滚到
ST_NOT_STARTED,后续任务仍可重试。 - 启动前重置空闲和忙碌周期计数器。
resetIdleCycles() :用于统计连续多少次轮询没有任务
resetBusyCycles() :用于统计连续多少次轮询都有任务执行
7. EventLoop 线程入口
真正的线程启动逻辑位于 doStartThread:
java
private void doStartThread() {
executor.execute(() -> {
processingLock.lock();
assert thread == null;
thread = Thread.currentThread();
if (interrupted) {
thread.interrupt();
interrupted = false;
}
boolean suspend = false;
try {
for (;;) {
SingleThreadEventExecutor.this.run();
int currentState = state;
if (canSuspend(currentState)) {
if (!STATE_UPDATER.compareAndSet(
this, ST_SUSPENDING, ST_SUSPENDED)) {
continue;
}
if (!canSuspend(ST_SUSPENDED) &&
STATE_UPDATER.compareAndSet(
this, ST_SUSPENDED, ST_STARTED)) {
continue;
}
suspend = true;
}
break;
}
} finally {
// 非暂停退出时进入优雅关闭和资源清理流程
// 最终状态会推进到 ST_TERMINATED
}
});
}
7.1 中断标志的处理
thread.interrupt() 不会强制终止 EventLoop,它只是设置线程的中断标志:
- 线程正在执行普通计算时,需要代码主动检查中断标志。
- 线程阻塞在
sleep()、wait()等可中断方法上时,通常会抛出InterruptedException,同时清除中断标志。 - 线程阻塞在 NIO
Selector.select()上时,中断会使选择操作返回。
8. EventLoop 主循环
SingleThreadIoEventLoop.run 是运行阶段的核心:
java
protected void run() {
assert inEventLoop();
ioHandler.initialize();
do {
// 处理 I/O 事件
runIo();
if (isShuttingDown()) {
ioHandler.prepareToDestroy();
}
// 执行普通任务和已到期的定时任务
runAllTasks(maxTaskProcessingQuantumNs);
// 检查关闭或暂停条件
} while (!confirmShutdown() && !canSuspend());
}
maxTaskProcessingQuantumNs 为任务执行设置时间配额,避免任务队列持续繁忙时长期占用线程,导致 I/O 事件得不到处理。
canSuspend(state) 方法不仅要求线程本身处于空闲状态,还强制要求当前 EventLoop 必须没有任何绑定的 Channel (numRegistrations == 0),才能安全地进入暂停状态。
9. Selector 选择策略
SingleThreadIoEventLoop.runIo() 最终调用 NioIoHandler.run(),并通过三种策略平衡响应速度与 CPU 占用:
CONTINUE:本轮不执行选择操作,直接进入下一轮。BUSY_WAIT:尝试忙等待;NIO 不支持时降级为SELECT。SELECT:允许线程阻塞,等待 I/O、超时或主动唤醒。
核心流程可以简化为:
java
int strategy = selectStrategy.calculateStrategy(
selectNowSupplier, !context.canBlock());
switch (strategy) {
case SelectStrategy.CONTINUE:
return 0;
case SelectStrategy.BUSY_WAIT:
// NIO 不支持真正的忙等待,继续执行 SELECT。
case SelectStrategy.SELECT:
select(context, wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
}
return processSelectedKeys();
9.1 select() 的截止时间与阻塞条件
select() 的超时时间由最近一个定时任务决定:
- 有定时任务:阻塞到最近任务的截止时间。
- 没有定时任务:调用
selector.select(0),无限期等待。 - 截止时间已到:调用一次
selectNow(),然后返回 EventLoop 执行任务。
一次阻塞选择可以概括为:
java
int selectedKeys = selector.select(timeoutMillis);
if (selectedKeys != 0 // I/O 就绪
|| oldWakenUp // 进入 select 前已被唤醒
|| wakenUp.get() // select 期间被唤醒
|| !runner.canBlock()) { // 已有任务需要处理
break;
}
如果线程被中断,线程会读取并清除中断标志。并且立即退出选择循环,并避免把中断造成的提前返回误判为空轮询。
9.2 selectNow、select 和 wakeup
selectNow():立即检查就绪事件,即使没有事件也会立刻返回0。select(timeout):最多阻塞指定时间;I/O 就绪、超时、线程中断或调用wakeup()时返回。selector.wakeup():唤醒正在阻塞的选择操作,使 EventLoop 及时处理跨线程提交的新任务。
NioIoHandler.wakeup() 只允许外部线程完成第一次有效唤醒:
java
public void wakeup() {
if (!executor.isExecutorThread(Thread.currentThread())
&& wakenUp.compareAndSet(false, true)) {
selector.wakeup();
}
}
这样既避免 EventLoop 唤醒自己,也避免多个外部线程重复调用 selector.wakeup()。runner.canBlock() 则确保存在待执行任务时尽量不进入阻塞选择。
9.3 wakenUp 双重检查
EventLoop 在进入 select() 前会用 wakenUp.getAndSet(false) 消费唤醒标志。该操作与真正进入阻塞之间存在竞争窗口,因此 select() 返回后还要再次检查 wakenUp;如果期间又发生唤醒,就再次调用 selector.wakeup(),避免下一轮误阻塞。
9.4 Selector 空轮询与自动重建
selectCnt 记录 select() 无事件、无任务、无唤醒、未超时却连续提前返回的次数。达到 SELECTOR_AUTO_REBUILD_THRESHOLD 后,Netty 会重建 Selector 并迁移已有的 SelectionKey,规避部分 JDK 实现可能出现的空轮询和 CPU 100% 问题。
正常超时、线程中断和主动唤醒不会计入空轮询;CancelledKeyException 只记录调试日志,不会终止 EventLoop。
10. 执行普通任务和定时任务
EventLoop 使用两个逻辑队列:
scheduledTaskQueue:保存尚未到期的定时任务。taskQueue:保存可以立即执行的普通任务。
执行任务前,先将已经到期的定时任务转移到普通任务队列:
java
protected boolean runAllTasks(long timeoutNanos) {
fetchFromScheduledTaskQueue(taskQueue);
Runnable task = pollTask();
if (task == null) {
afterRunningAllTasks();
return false;
}
long deadline = timeoutNanos > 0
? getCurrentTimeNanos() + timeoutNanos
: 0;
long runTasks = 0;
long lastExecutionTime;
long workStartTime = ticker().nanoTime();
for (;;) {
safeExecute(task);
runTasks++;
// 每执行 64 个任务检查一次时间配额
if ((runTasks & 0x3F) == 0) {
lastExecutionTime = getCurrentTimeNanos();
if (lastExecutionTime >= deadline) {
break;
}
}
task = pollTask();
if (task == null) {
lastExecutionTime = getCurrentTimeNanos();
break;
}
}
long workEndTime = ticker().nanoTime();
accumulatedActiveTimeNanos += workEndTime - workStartTime;
lastActivityTimeNanos = workEndTime;
this.lastExecutionTime = lastExecutionTime;
afterRunningAllTasks();
return true;
}
这段代码的作用:
- 定时任务到期后与普通任务使用同一执行流程。
- 使用
poll()非阻塞取任务,避免任务队列阻塞 I/O 处理。 - 使用
safeExecute()隔离单个任务异常,避免 EventLoop 意外退出。 - 每 64 个任务检查一次时间,减少频繁读取系统时钟的开销,同时限制任务阶段的最长执行时间。
11. 优雅关闭
优雅关闭不是立刻停止线程,而是先处理剩余任务,再释放资源。
调用 shutdownGracefully() 后,EventLoop 会被唤醒,并在自己的线程中反复调用 confirmShutdown() 检查能否退出。
整个过程可以简化为:
java
protected boolean confirmShutdown() {
if (!isShuttingDown()) {
return false;
}
// 关闭阶段不再等待尚未到期的定时任务。
cancelScheduledTasks();
if (gracefulShutdownStartTime == 0) {
gracefulShutdownStartTime = now();
}
// 尽量执行队列中剩余的任务和关闭钩子。
if (runAllTasks() || runShutdownHooks()) {
taskQueue.offer(WAKEUP_TASK);
return false;
}
// 等待时间超过 timeout,不再继续等。
if (now() - gracefulShutdownStartTime > gracefulShutdownTimeout) {
return true;
}
// quietPeriod 内执行过新任务,还要继续等待。
if (now() - lastExecutionTime <= gracefulShutdownQuietPeriod) {
taskQueue.offer(WAKEUP_TASK);
return false;
}
// 已经连续一个 quietPeriod 没有新任务,可以退出。
return true;
}
上面是便于理解的简化代码,省略了强制关闭、线程检查和短暂休眠等细节。
11.1 quietPeriod 和 timeout
quietPeriod:安静等待时间。在这段时间内如果又执行了新任务,就重新开始计时。timeout:最多等待多久。到达这个时间后,不再继续等待安静期。
例如:
java
shutdownGracefully(2, 15, TimeUnit.SECONDS);
它表示:最后一次执行任务后,连续 2 秒没有新任务就关闭;无论是否还有新任务,整个关闭过程最多等待 15 秒。
11.2 WAKEUP_TASK 的作用
WAKEUP_TASK 本身不做业务处理,只负责让 EventLoop 不要一直阻塞。EventLoop 被唤醒后,会再次检查任务队列和关闭条件。
11.3 完整关闭步骤
- 外部线程调用
shutdownGracefully(),发出关闭请求并唤醒 EventLoop。 - EventLoop 取消未到期的定时任务。
- EventLoop 执行队列中剩余的普通任务和关闭钩子。
- 没有新任务达到
quietPeriod,或者总时间达到timeout后,退出主循环。 cleanup()关闭 Selector、Channel 等资源。- 清理线程本地变量,并完成
terminationFuture,通知等待关闭结果的调用方。
12. 生命周期总结
EventLoop 的完整生命周期可以归纳为:
text
1. EventLoopGroup 创建多个子 EventLoop
2. Channel 注册并绑定其中一个 EventLoop
3. 外部线程提交注册任务或普通任务
4. EventLoop 使用 CAS 延迟启动线程
5. EventLoop 在线程中循环执行:
- 选择并处理 I/O 事件
- 转移已到期的定时任务
- 执行普通任务
- 检查暂停和关闭条件
6. shutdownGracefully() 触发优雅关闭
7. 执行剩余任务、关闭钩子和资源清理
8. 状态进入 ST_TERMINATED
9. terminationFuture 完成