7.EventLoop 生命周期

EventLoop 生命周期

整体流程如下:

text 复制代码
创建 EventLoopGroup
        ↓
创建多个子 EventLoop,此时通常并不会启动线程
        ↓
Channel 注册或外部线程提交任务
        ↓
EventLoop 延迟启动线程
        ↓
循环处理 I/O 事件、普通任务和定时任务
        ↓
执行优雅关闭
        ↓
释放资源并完成

1. EventLoop 的线程模型

理解生命周期之前,需要先明确三个对象之间的关系:

  • EventLoopGroup:管理一组 EventLoop,并负责选择一个 EventLoop 处理新注册的 Channel
  • EventLoop:本质上是单线程事件执行器,持续处理 I/O 事件、普通任务和定时任务。
  • Channel:注册成功后会绑定到某个 EventLoop,后续生命周期事件和 I/O 操作通常都由该线程处理。

EventLoopGroup 可以类比为线程池,但与普通线程池并不完全相同:

  1. 每个 SingleThreadIoEventLoop 同一时刻只有一个线程执行。
  2. 一个 EventLoop 可以被多个 Channel注册。
  3. 一个 Channel 注册在EventLoop后便不再发生改变。
  4. 同一 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_SUSPENDINGST_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);
    }
}

这段代码完成了四件事:

  1. 创建底层 Executor,用于真正启动 EventLoop 线程。
  2. 创建指定数量的子 EventLoop
  3. 创建 chooser,用于为新任务或新连接选择 EventLoop
  4. 监听所有子 EventLoop 的终止状态。

此时创建的是 EventLoop 对象,而不是立即启动所有线程。线程采用延迟启动策略,只有注册 Channel 或提交任务时才会真正启动。

每个子 EventLoop 都有一个只读的 terminationFuture。当最后一个子 EventLoop 终止时,EventLoopGroupterminationFuture 才会完成,它表示的是"执行器已经终止"

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 活动时间。

addTaskWakesUpfalse 表示任务入队不会隐式唤醒线程;SingleThreadIoEventLoop 会根据任务是否需要立即执行,通过 NioIoHandler.wakeup() 有条件地唤醒 Selector。

5. Channel 注册与任务提交

Channel 注册后会绑定到一个 EventLoop。其 channelRegisteredchannelActive、读写事件以及后续的 channelInactivechannelUnregistered 通常都由绑定的 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);
    }
}

任务提交流程:

  1. 判断当前线程是否就是 EventLoop 线程。
  2. 将任务加入 taskQueue
  3. 外部线程提交任务时,确保 EventLoop 已经启动。
  4. 如果执行器已经关闭,尝试移除并拒绝刚提交的任务。
  5. 必要时唤醒阻塞在 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);
    }
}

这段代码说明:

  1. 真正的 ioHandler.register(handle) 始终在 EventLoop 线程执行。
  2. 外部线程调用注册方法时,返回一个尚未完成的 Promise
  3. 注册成功后递增 numRegistrations,并通过 Promise 通知结果。
  4. 注册失败时调用 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);
                }
            }
        }
    }
}

这里有三个关键点:

  1. 使用 CAS 保证并发提交任务时只有一个线程负责启动 EventLoop。
  2. 启动失败时回滚到 ST_NOT_STARTED,后续任务仍可重试。
  3. 启动前重置空闲和忙碌周期计数器。

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 必须没有任何绑定的 ChannelnumRegistrations == 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() 的超时时间由最近一个定时任务决定:

  1. 有定时任务:阻塞到最近任务的截止时间。
  2. 没有定时任务:调用 selector.select(0),无限期等待。
  3. 截止时间已到:调用一次 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;
}

这段代码的作用:

  1. 定时任务到期后与普通任务使用同一执行流程。
  2. 使用 poll() 非阻塞取任务,避免任务队列阻塞 I/O 处理。
  3. 使用 safeExecute() 隔离单个任务异常,避免 EventLoop 意外退出。
  4. 每 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 完整关闭步骤

  1. 外部线程调用 shutdownGracefully(),发出关闭请求并唤醒 EventLoop。
  2. EventLoop 取消未到期的定时任务。
  3. EventLoop 执行队列中剩余的普通任务和关闭钩子。
  4. 没有新任务达到 quietPeriod,或者总时间达到 timeout 后,退出主循环。
  5. cleanup() 关闭 Selector、Channel 等资源。
  6. 清理线程本地变量,并完成 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 完成
相关推荐
雨落倾城夏未凉1 小时前
halcon核心-模板匹配定位(二)
后端
ma_king1 小时前
Spring Boot 接入飞书自定义机器人
java·后端
2401_894915531 小时前
部署 GEO 优化源码常见报错排查:端口、伪静态、缓存问题解决
java·运维·服务器·后端·缓存·开源
卷无止境1 小时前
聊聊Web开发里的流式数据 从原理到FastAPI实战
后端·python·fastapi
Scene2161 小时前
AgentScope 2.0:4. Message & Event —— 消息模型与事件流深度解析
后端
元界metalite1 小时前
MyBatis 字段改名为何查询不报错?MetaLite ORM 如何做到类型安全?
后端
Csvn2 小时前
🐍 Day 2 :Python 变量与数据类型 — 一切皆对象
后端
Csvn2 小时前
📊 SQL 入门 Day 17:数据更新与删除
后端·sql
秋天的一阵风2 小时前
🔥 Network 里那坨 "data:" 我真看吐了,自制开源 Chrome 插件,AI 流式调试直接开挂
前端·人工智能·后端