Netty 第八篇:doBind0() 与 EventLoop 的线程启动

📅 2026-05-22    ✍️ 原创    🏷️ Java / NIO / Netty / 源码分析 / doBind0

在上一篇文章中,我们详细分析了 Channel 是如何注册到 Selector 上的。注册完成后,服务端还不能接收连接,因为我们还没有真正监听端口。

这一篇,我们来看看 bind(8080) 的最后一步:doBind0()。这一步不仅会真正监听端口,还会揭开 EventLoop 线程是如何启动的这个谜题。

一句话剧透:doBind0 并没有直接调用 JDK 的 bind 方法,而是向 EventLoop 提交了一个任务。

一、doBind0() 的入口

在 AbstractBootstrap 中,注册完成后会调用 doBind0:

private static void doBind0(
        final ChannelFuture regFuture, final Channel channel,
        final SocketAddress localAddress, final ChannelPromise promise) {

    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                channel.bind(localAddress, promise);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

二、SingleThreadEventExecutor 的核心逻辑

当我们调用 channel.eventLoop().execute() 时,实际上是调用了 SingleThreadEventExecutor.execute()。

@Override
public void execute(Runnable task) {
    boolean inEventLoop = inEventLoop();
    addTask(task);
    if (!inEventLoop) {
        startThread();
    }
}
💡 注意:此时我们的主线程正在执行 bind() 方法,而 inEventLoop() 判断的是当前线程是否是 EventLoop 绑定的那个唯一的线程。显然,主线程不是,所以会触发 startThread()。

三、doStartThread():线程的诞生

这是 Netty 线程模型的灵魂所在。在 doStartThread() 中,Netty 并没有直接 new Thread(),而是使用了 executor。

private void doStartThread() {
    executor.execute(new Runnable() {
        @Override
        public void run() {
            SingleThreadEventExecutor.this.thread = Thread.currentThread();
            SingleThreadEventExecutor.this.run();
        }
    });
}

这里的 executor 通常是 ThreadPerTaskExecutor,它的作用就是每次执行任务时创建一个新的线程。SingleThreadEventExecutor.this.thread = Thread.currentThread();这里完成了EventLoop与线程的绑定。

SingleThreadEventExecutor.this.run();会进入死循环,用于处理当前EventLoop中的IO事件以及任务。这一点在下一章说明。

四、bind0 任务的执行

当 EventLoop 线程启动后,它就会从任务队列里取出我们在 doBind0 中提交的那个任务并执行:

if (regFuture.isSuccess()) {
    channel.bind(localAddress, promise);
}

五、Pipeline 的 bind 传播

channel.bind() 会调用 pipeline.bind()。Pipeline 的传播顺序是从尾到头,最终传播到HeadContext:

@Override
public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
    tail.bind(localAddress, promise);
    return promise;
}

六、HeadContext 与 Unsafe

在 HeadContext 中,真正的绑定逻辑交给了 unsafe:

@Override
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) {
    unsafe.bind(localAddress, promise);
}

七、NioServerSocketChannel.doBind()

终于,我们来到了最底层。在 AbstractUnsafe.bind() 中,会调用 AbstractChannel的doBind()方法,这是一个抽象方法,最终执行类为NioServerSocketChannel。

@Override
protected void doBind(SocketAddress localAddress) throws Exception {
    javaChannel().bind(localAddress, config.getBacklog());
}

至此,javaChannel()(即 JDK 的 ServerSocketChannel)正式调用 bind() 方法,操作系统开始监听 8080 端口。

八、总结

阶段核心动作
提交任务 主线程向 EventLoop 提交 bind 任务
启动线程 SingleThreadEventExecutor.startThread()
绑定线程 将ThreadPerTaskExecutor生成的线程赋值给EventLoop.thread并启动run方法处理IO事件与任务
Pipeline 传播 调用bind方法,传播路径:Channel→Pipeline→Tail → Head→Unsafe→AbstractChannel→NioServerSocketChannel
JDK 绑定 ServerSocketChannel.bind()
💡 下篇预告:EventLoop 线程启动后,会执行 run() 方法进入死循环。这个死循环里究竟在干什么?我们下一篇揭晓。

— 原创文章,转载请注明出处 —

posted @ 2026-05-29 14:59  小金虫  阅读(8)  评论(0)    收藏  举报