认真的 Netty 源码解析(一)

任务结束,结果:123456

任务结束,balabala…

读者这里可以试一下 sync() 和 await() 的区别,在任务中调用 promise.setFailure(new RuntimeException()) 试试看。

上面的代码中,大家可能会对线程池 executor 和 promise 之间的关系感到有点迷惑。读者应该也要清楚,具体的任务不一定就要在这个 executor 中被执行。任务结束以后,需要调用 promise.setSuccess(result) 作为通知。

通常来说,promise 代表的 future 是不需要和线程池搅在一起的,future 只关心任务是否结束以及任务的执行结果,至于是哪个线程或哪个线程池执行的任务,future 其实是不关心的。

不过 Netty 毕竟不是要创建一个通用的线程池实现,而是和它要处理的 IO 息息相关的,所以我们只不过要理解它就好了。

这节就说这么多吧,我们回过头来再看一下这张图,看看大家是不是看懂了这节内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们就说说上图左边的部分吧,虽然我们还不知道 bind() 操作中具体会做什么工作,但是我们应该可以猜出一二。

显然,main 线程调用 b.bind(port) 这个方法会返回一个 ChannelFuture,bind() 是一个异步方法,当某个执行线程执行了真正的绑定操作后,那个执行线程一定会标记这个 future 为成功(我们假定 bind 会成功),然后这里的 sync() 方法就会返回了。

如果 bind(port) 失败,我们知道,sync() 方法会将异常抛出来,然后就会执行到 finally 块了。

一旦绑定端口成功,进入下面一行,f.channel() 方法会返回该 future 关联的 channel。channel.closeFuture() 也会返回一个 ChannelFuture,然后调用了 sync() 方法,这个 sync() 方法返回的条件是:有其他的线程关闭了 NioServerSocketChannel,往往是因为需要停掉服务了,然后那个线程会设置 future 的状态( setSuccess(result) 或 setFailure(cause) ),这个 sync() 方法才会返回。

这节就到这里,希望大家对 Netty 中的异步编程有些了解以后,后续碰到源码的时候能知道是怎么使用的。

ChannelPipeline,和 Inbound、Outbound


我想很多读者应该或多或少都有 Netty 中 pipeline 的概念。前面我们说了,使用 Netty 的时候,我们通常就只要写一些自定义的 handler 就可以了,我们定义的这些 handler 会组成一个 pipeline,用于处理 IO 事件,这个和我们平时接触的 Filter 或 Interceptor 表达的差不多是一个意思。

每个 Channel 内部都有一个 pipeline,pipeline 由多个 handler 组成,handler 之间的顺序是很重要的,因为 IO 事件将按照顺序顺次经过 pipeline 上的 handler,这样每个 handler 可以专注于做一点点小事,由多个 handler 组合来完成一些复杂的逻辑。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先,我们看两个重要的概念:Inbound 和 Outbound。在 Netty 中,IO 事件被分为 Inbound 事件和 Outbound 事件。

Outbound 的 out 指的是 出去,有哪些 IO 事件属于此类呢?比如 connect、write、flush 这些 IO 操作是往外部方向进行的,它们就属于 Outbound 事件。

其他的,诸如 accept、read 这种就属于 Inbound 事件。

比如客户端在发起请求的时候,需要 1️⃣connect 到服务器,然后 2️⃣write 数据传到服务器,再然后 3️⃣read 服务器返回的数据,前面的 connect 和 write 就是 out 事件,后面的 read 就是 in 事件。

比如很多初学者看不懂下面的这段代码,这段代码用于服务端的 childHandler 中:

1. pipeline.addLast(new StringDecoder());

  1. pipeline.addLast(new StringEncoder());

  2. pipeline.addLast(new BizHandler());

初学者肯定都纳闷,以为这个顺序写错了,应该是先 decode 客户端过来的数据,然后用 BizHandler 处理业务逻辑,最后再 encode 数据然后返回给客户端,所以添加的顺序应该是 1 -> 3 -> 2 才对。

其实这里的三个 handler 是分组的,分为 Inbound(1 和 3) 和 Outbound(2):

  • 客户端连接进来的时候,读取(read)客户端请求数据的操作是 Inbound 的,所以会先使用 1,然后是 3 对处理进行处理;

  • 处理完数据后,返回给客户端数据的 write 操作是 Outbound 的,此时使用的是 2。

所以虽然添加顺序有点怪,但是执行顺序其实是按照 1 -> 3 -> 2 进行的。

如果我们在上面的基础上,加上下面的第四行,这是一个 OutboundHandler:

4. pipeline.addLast(new OutboundHandlerA());

那么执行顺序是不是就是 1 -> 3 -> 2 -> 4 呢?答案是:不是的。

对于 Inbound 操作,按照添加顺序执行每个 Inbound 类型的 handler;而对于 Outbound 操作,是反着来的,从后往前,顺次执行 Outbound 类型的 handler。

所以,上面的顺序应该是先 1 后 3,它们是 Inbound 的,然后是 4,最后才是 2,它们两个是 Outbound 的。

到这里,我想大家应该都知道 Inbound 和 Outbound 了吧?下面我们来介绍它们的接口使用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

定义处理 Inbound 事件的 handler 需要实现 ChannelInboundHandler,定义处理 Outbound 事件的 handler 需要实现 ChannelOutboundHandler。最下面的三个类,是 Netty 提供的适配器,特别的,如果我们希望定义一个 handler 能同时处理 Inbound 和 Outbound 事件,可以通过继承中间的 ChannelDuplexHandler 的方式。

有了 Inbound 和 Outbound 的概念以后,我们来开始介绍 Pipeline 的源码。

我们说过,一个 Channel 关联一个 pipeline,NioSocketChannel 和 NioServerSocketChannel 在执行构造方法的时候,都会走到它们的父类 AbstractChannel 的构造方法中:

protected AbstractChannel(Channel parent) {

this.parent = parent;

// 给每个 channel 分配一个唯一 id

id = newId();

// 每个 channel 内部需要一个 Unsafe 的实例

unsafe = newUnsafe();

// 每个 channel 内部都会创建一个 pipeline

pipeline = newChannelPipeline();

}

上面的三行代码中,id 比较不重要,Netty 中的 Unsafe 实例其实挺重要的,这里简单介绍一下。

在 JDK 的源码中,sun.misc.Unsafe 类提供了一些底层操作的能力,它设计出来是给 JDK 中的源码使用的,比如 AQS、ConcurrentHashMap 等,我们在之前的并发包的源码分析中也看到了很多它们使用 Unsafe 的场景,这个 Unsafe 类不是给我们的代码使用的(需要的话,我们也是可以获取它的实例的)。

Unsafe 类的构造方法是 private 的,但是它提供了 getUnsafe() 这个静态方法:

Unsafe unsafe = Unsafe.getUnsafe();

大家可以试一下,上面这行代码编译没有问题,但是执行的时候会抛 java.lang.SecurityException 异常,因为它就不是给我们的代码用的。

但是如果你就是想获取 Unsafe 的实例,可以通过下面这个代码获取到:

Field f = Unsafe.class.getDeclaredField(“theUnsafe”);

f.setAccessible(true);

Unsafe unsafe = (Unsafe) f.get(null);

Netty 中的 Unsafe 也是同样的意思,它封装了 Netty 中会使用到的 JDK 提供的 NIO 接口,比如将 channel 注册到 selector 上,比如 bind 操作,比如 connect 操作等,这些操作都是稍微偏底层一些。Netty 同样也是不希望我们的业务代码使用 Unsafe 的实例,它是提供给 Netty 中的源码使用的。

不过,对于我们源码分析来说,我们还是会有很多时候需要分析 Unsafe 中的源码的

关于 Unsafe,我们后面用到了再说,这里只要知道,它封装了大部分需要访问 JDK 的 NIO 接口的操作就好了。这里我们继续将焦点放在 pipeline 上:

protected DefaultChannelPipeline newChannelPipeline() {

return new DefaultChannelPipeline(this);

}

这里开始调用 DefaultChannelPipeline 的构造方法,并把当前 channel 的引用传入:

protected DefaultChannelPipeline(Channel channel) {

this.channel = ObjectUtil.checkNotNull(channel, “channel”);

succeededFuture = new SucceededChannelFuture(channel, null);

voidPromise = new VoidChannelPromise(channel, true);

tail = new TailContext(this);

head = new HeadContext(this);

head.next = tail;

tail.prev = head;

}

这里实例化了 tail 和 head 这两个 handler。tail 实现了 ChannelInboundHandler 接口,而 head 实现了 ChannelOutboundHandler 和 ChannelInboundHandler 两个接口,并且最后两行代码将 tail 和 head 连接起来:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意,在不同的版本中,源码也略有差异,head 不一定是 in + out,大家知道这点就好了。

还有,从上面的 head 和 tail 我们也可以看到,其实 pipeline 中的每个元素是 ChannelHandlerContext 的实例,而不是 ChannelHandler 的实例,context 包装了一下 handler,但是,后面我们都会用 handler 来描述一个 pipeline 上的节点,而不是使用 context,希望读者知道这一点。

这里只是构造了 pipeline,并且添加了两个固定的 handler 到其中(head + tail),还不涉及到自定义的 handler 代码执行。我们回过头来看下面这段代码:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们说过 childHandler 中指定的 handler 不是给 NioServerSocketChannel 使用的,是给 NioSocketChannel 使用的,所以这里我们不看它。

这里调用 handler(…) 方法指定了一个 LoggingHandler 的实例,然后我们再进去下面的 bind(…) 方法中看看这个 LoggingHandler 实例是怎么进入到我们之前构造的 pipeline 内的。

顺着 bind() 一直往前走,bind() -> doBind() -> initAndRegister():

final ChannelFuture initAndRegister() {

Channel channel = null;

try {

// 1. 构造 channel 实例,同时会构造 pipeline 实例,

// 现在 pipeline 中有 head 和 tail 两个 handler 了

channel = channelFactory.newChannel();

// 2. 看这里

init(channel);

} catch (Throwable t) {

}

上面的两行代码,第一行实现了构造 channel 和 channel 内部的 pipeline,我们来看第二行 init 代码:

// ServerBootstrap:

@Override

void init(Channel channel) throws Exception {

// 拿到刚刚创建的 channel 内部的 pipeline 实例

ChannelPipeline p = channel.pipeline();

// 开始往 pipeline 中添加一个 handler,这个 handler 是 ChannelInitializer 的实例

p.addLast(new ChannelInitializer() {

// 我们以后会看到,下面这个 initChannel 方法何时会被调用

@Override

public void initChannel(final Channel ch) throws Exception {

final ChannelPipeline pipeline = ch.pipeline();

// 这个方法返回我们最开始指定的 LoggingHandler 实例

ChannelHandler handler = config.handler();

if (handler != null) {

// 添加 LoggingHandler

pipeline.addLast(handler);

}

// 先不用管这里的 eventLoop

ch.eventLoop().execute(new Runnable() {

@Override

public void run() {

// 添加一个 handler 到 pipeline 中:ServerBootstrapAcceptor

// 从名字可以看到,这个 handler 的目的是用于接收客户端请求

pipeline.addLast(new ServerBootstrapAcceptor(

ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));

}

});

}

});

}

这里涉及到 pipeline 中的辅助类 ChannelInitializer,我们看到,它本身是一个 handler(Inbound 类型),但是它的作用和普通 handler 有点不一样,它纯碎是用来将其他的 handler 加入到 pipeline 中的。

此时的 pipeline 应该是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ChannelInitializer 的 initChannel(channel) 方法被调用的时候,会往 pipeline 中添加我们最开始指定的 LoggingHandler 和添加一个 ServerBootstrapAcceptor。但是我们现在还不知道这个 initChannel 方法何时会被调用。

上面我们说的是作为服务端的 NioServerSocketChannel 的 pipeline,NioSocketChannel 也是差不多的,我们可以看一下 Bootstrap 类的 init(channel) 方法:

void init(Channel channel) throws Exception {

ChannelPipeline p = channel.pipeline();

p.addLast(config.handler());

}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

它和服务端 ServerBootstrap 要添加 ServerBootstrapAcceptor 不一样,它只需要将 EchoClient 类中的 ChannelInitializer 实例加进来就可以了,它的 ChannelInitializer 中添加了两个 handler,LoggingHandler 和 EchoClientHandler:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

很显然,我们需要的是像 LoggingHandler 和 EchoClientHandler 这样的 handler,但是,它们现在还不在 pipeline 中,那么它们什么时候会真正进入到 pipeline 中呢?以后我们再揭晓。

还有,为什么 Server 端我们指定的是一个 handler 实例,而 Client 指定的是一个 ChannelInitializer 实例?其实它们是可以随意搭配使用的,你甚至可以在 ChannelInitializer 实例中添加 ChannelInitializer 的实例。

非常抱歉,这里又要断了,下面要先介绍线程池了,大家要记住 pipeline 现在的样子,head + channelInitializer + tail

本节没有介绍 handler 的向后传播,就是一个 handler 处理完了以后,怎么传递给下一个 handler 来处理?比如我们熟悉的 JavaEE 中的 Filter 是采用在一个 Filter 实例中调用 chain.doFilter(request, response) 来传递给下一个 Filter 这种方式的。

我们用下面这张图结束本节。下图展示了传播的方法,但我其实是更想让大家看一下,哪些事件是 Inbound 类型的,哪些是 Outbound 类型的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Outbound 类型大家应该比较好认,注意 bind 也是 Outbound 类型的。

Netty 中的线程池 EventLoopGroup


接下来,我们来分析 Netty 中的线程池。Netty 中的线程池比较不好理解,因为它的类比较多,而且它们之间的关系错综复杂。看下图,感受下 NioEventLoop 类和 NioEventLoopGroup 类的继承结构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这张图我整理得有些乱,但是大家仔细看一下就会发现,涉及到的类确实挺多的。本节来给大家理理清楚这部分内容。

首先,我们说的 Netty 的线程池,指的就是 NioEventLoopGroup 的实例;线程池中的单个线程,指的是右边 NioEventLoop 的实例。

我们第一节介绍的 Echo 例子,客户端和服务端的启动代码中,最开始我们总是先实例化 NioEventLoopGroup:

// EchoClient 代码最开始:

EventLoopGroup group = new NioEventLoopGroup();

// EchoServer 代码最开始:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);

EventLoopGroup workerGroup = new NioEventLoopGroup();

下面,我们就从 NioEventLoopGroup 的源码开始进行分析。

我们打开 NioEventLoopGroup 的源码,可以看到,NioEventLoopGroup 有多个构造方法用于参数设置,最简单地,我们采用无参构造函数,或仅仅设置线程数量就可以了,其他的参数采用默认值。

public NioEventLoopGroup() {

this(0);

}

public NioEventLoopGroup(int nThreads) {

this(nThreads, (Executor) null);

}

// 参数最全的构造方法

public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory,

final SelectorProvider selectorProvider,

final SelectStrategyFactory selectStrategyFactory,

final RejectedExecutionHandler rejectedExecutionHandler) {

// 调用父类的构造方法

super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, rejectedExecutionHandler);

}

我们来稍微看一下构造方法中的各个参数:

  • nThreads:这个最简单,就是线程池中的线程数,也就是 NioEventLoop 的实例数量。

  • executor:我们知道,我们本身就是要构造一个线程池(Executor),为什么这里传一个 executor 实例呢?它其实不是给线程池用的,而是给 NioEventLoop 用的。

  • chooserFactory:当我们提交一个任务到线程池的时候,线程池需要选择(choose)其中的一个线程来执行这个任务,这个就是用来实现选择策略的。

  • selectorProvider:这个简单,我们需要通过它来实例化 Selector,可以看到每个线程池都持有一个 selectorProvider 实例。

  • selectStrategyFactory:这个涉及到的是线程池中线程的工作流程,在介绍 NioEventLoop 的时候会说。

  • rejectedExecutionHandler:这个也是线程池的好朋友了,用于处理线程池中没有可用的线程来执行任务的情况。在 Netty 中稍微有一点点不一样,这个是给 NioEventLoop 实例用的,以后我们再详细介绍。

这里介绍这些参数是希望大家有个印象,这样可能会对接下来的源码更有感觉一些,我们接下来就追着一条线走下去看看。

我们就看无参构造方法:

public NioEventLoopGroup() {

this(0);

}

然后一步步走下去,到这个构造方法:

public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory, final SelectorProvider selectorProvider, final SelectStrategyFactory selectStrategyFactory) {

super(nThreads, threadFactory, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());

}

大家自己要去跟一下源码,这样才知道设置了哪些默认值,下面这几个参数都被设置了默认值:

  • selectorProvider = SelectorProvider.provider()

这个没什么好说的,调用了 JDK 提供的方法

  • selectStrategyFactory = DefaultSelectStrategyFactory.INSTANCE

这个涉及到的是线程在做 select 操作和执行任务过程中的策略选择问题,在介绍 NioEventLoop 的时候会用到。

  • rejectedExecutionHandler = RejectedExecutionHandlers.reject()

也就是说,默认拒绝策略是:抛出异常

跟着源码走,我们会来到父类 MultithreadEventLoopGroup 的构造方法中:

protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object… args) {

super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);

}

这里我们发现,如果采用无参构造函数,那么到这里的时候,默认地 nThreads 会被设置为 CPU 核心数 *2。大家可以看下 DEFAULT_EVENT_LOOP_THREADS 的默认值,以及 static 代码块的设值逻辑。

我们继续往下走:

protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object… args) {

this(nThreads, threadFactory == null ? null : new ThreadPerTaskExecutor(threadFactory), args);

}

到这一步的时候,new ThreadPerTaskExecutor(threadFactory) 会构造一个 executor。

我们现在还不知道这个 executor 怎么用,我们看下它的源码:

public final class ThreadPerTaskExecutor implements Executor {

private final ThreadFactory threadFactory;
public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
    if (threadFactory == null) {
        throw new NullPointerException("threadFactory");
    }
    this.threadFactory = threadFactory;
}
@Override
public void execute(Runnable command) {
    // 为每个任务新建一个线程
    threadFactory.newThread(command).start();
}

}

Executor 作为线程池的最顶层接口, 我们知道,它只有一个 execute(runnable) 方法,从上面我们可以看到,实现类 ThreadPerTaskExecutor 的逻辑就是每来一个任务,新建一个线程

我们先记住这个,前面也说了,它是给 NioEventLoop 用的,不是给 NioEventLoopGroup 用的。

上一步设置完了 executor,我们继续往下看:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object… args) {

this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);

}

这一步设置了 chooserFactory,用来实现从线程池中选择一个线程的选择策略。

ChooserFactory 的逻辑比较简单,我们看下 DefaultEventExecutorChooserFactory 的实现:

@Override

public EventExecutorChooser newChooser(EventExecutor[] executors) {

if (isPowerOfTwo(executors.length)) {
    return new PowerOfTwoEventExecutorChooser(executors);
} else {
    return new GenericEventExecutorChooser(executors);
}

}

这里设置的策略也很简单:

1、如果线程池的线程数量是 2^n,采用下面的方式会高效一些:

@Override

public EventExecutor next() {

return executors\[idx.getAndIncrement() & executors.length - 1\];

}

2、如果不是,用取模的方式:

@Override

public EventExecutor next() {

return executors\[Math.abs(idx.getAndIncrement() % executors.length)\];

}

走了这么久,我们终于到了一个干实事的构造方法中了:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,

EventExecutorChooserFactory chooserFactory, Object… args) {

if (nThreads <= 0) {

throw new IllegalArgumentException(String.format(“nThreads: %d (expected: > 0)”, nThreads));

}

// executor 如果是 null,做一次和前面一样的默认设置。

if (executor == null) {

executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());

}

// 这里的 children 数组非常重要,它就是线程池中的线程数组,这么说不太严谨,但是就大概这个意思

children = new EventExecutor[nThreads];

// 下面这个 for 循环将实例化 children 数组中的每一个元素

for (int i = 0; i < nThreads; i ++) {

boolean success = false;

try {

// 实例化!!!!!!

children[i] = newChild(executor, args);

success = true;

} catch (Exception e) {

// TODO: Think about if this is a good exception type

throw new IllegalStateException(“failed to create a child event loop”, e);

} finally {

// 如果有一个 child 实例化失败,那么 success 就会为 false,然后进入下面的失败处理逻辑

if (!success) {

// 把已经成功实例化的“线程” shutdown,shutdown 是异步操作

for (int j = 0; j < i; j ++) {

children[j].shutdownGracefully();

}

// 等待这些线程成功 shutdown

for (int j = 0; j < i; j ++) {

EventExecutor e = children[j];

try {

while (!e.isTerminated()) {

e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);

}

} catch (InterruptedException interrupted) {

// 把中断状态设置回去,交给关心的线程来处理.

Thread.currentThread().interrupt();

break;

}

}

}

}

}

// ================================================

// === 到这里,就是代表上面的实例化所有线程已经成功结束 ===

// ================================================

// 通过之前设置的 chooserFactory 来实例化 Chooser,把线程池数组传进去,这就不必再说了吧

chooser = chooserFactory.newChooser(children);

// 设置一个 Listener 用来监听该线程池的 termination 事件

// 下面的代码逻辑是:给池中每一个线程都设置这个 listener,当监听到所有线程都 terminate 以后,这个线程池就算真正的 terminate 了。

final FutureListener terminationListener = new FutureListener() {

@Override

public void operationComplete(Future future) throws Exception {

if (terminatedChildren.incrementAndGet() == children.length) {

terminationFuture.setSuccess(null);

}

}

};

for (EventExecutor e: children) {

e.terminationFuture().addListener(terminationListener);

}

// 设置 readonlyChildren,它是只读集合,以后用到再说

Set childrenSet = new LinkedHashSet(children.length);

Collections.addAll(childrenSet, children);

readonlyChildren = Collections.unmodifiableSet(childrenSet);

}

上面的代码非常简单,没有什么需要特别说的,接下来,我们来看看 newChild() 这个方法,这个方法非常重要,它将创建线程池中的线程。

我上面已经用过很多次"线程"这个词了,它可不是 Thread 的意思,而是指池中的个体,后面我们会看到每个"线程"在什么时候会真正创建 Thread 实例。反正每个 NioEventLoop 实例内部都会有一个自己的 Thread 实例,所以把这两个概念混在一起也无所谓吧。

newChild(…) 方法在 NioEventLoopGroup 中覆写了,上面说的"线程"其实就是 NioEventLoop:

@Override

protected EventLoop newChild(Executor executor, Object… args) throws Exception {

return new NioEventLoop(this, executor, (SelectorProvider) args[0],

((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);

}

它调用了 NioEventLoop 的构造方法:

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,

SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {

super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);

if (selectorProvider == null) {

throw new NullPointerException(“selectorProvider”);

}

if (strategy == null) {

throw new NullPointerException(“selectStrategy”);

}

provider = selectorProvider;

// 开启 NIO 中最重要的组件:Selector

final SelectorTuple selectorTuple = openSelector();

selector = selectorTuple.selector;

unwrappedSelector = selectorTuple.unwrappedSelector;

selectStrategy = strategy;

}

我们先粗略观察一下,然后再往下看:

  • 在 Netty 中,NioEventLoopGroup 代表线程池,NioEventLoop 就是其中的线程。

  • 线程池 NioEventLoopGroup 是池中的线程 NioEventLoop 的 parent,从上面的代码中的取名可以看出。

  • 每个 NioEventLoop 都有自己的 Selector,上面的代码也反应了这一点,这和 Tomcat 中的 NIO 模型有点区别。

  • executor、selectStrategy 和 rejectedExecutionHandler 从 NioEventLoopGroup 中一路传到了 NioEventLoop 中。

这个时候,我们来看一下 NioEventLoop 的属性都有哪些,我们先忽略它的父类的属性,单单看它自己的:

private Selector selector;

private Selector unwrappedSelector;

private SelectedSelectionKeySet selectedKeys;

private final SelectorProvider provider;

private final AtomicBoolean wakenUp = new AtomicBoolean();

private final SelectStrategy selectStrategy;

private volatile int ioRatio = 50;

private int cancelledKeys;

private boolean needsToSelectAgain;

结合它的构造方法我们来总结一下:

  • provider:它由 NioEventLoopGroup 传进来,前面我们说了一个线程池有一个 selectorProvider,用于创建 Selector 实例

  • selector:虽然我们还没看创建 selector 的代码,但我们已经知道,在 Netty 中 Selector 是跟着线程池中的线程走的。

  • selectStrategy:select 操作的策略,这个不急。

  • ioRatio:这是 IO 任务的执行时间比例,因为每个线程既有 IO 任务执行,也有非 IO 任务需要执行,所以该参数为了保证有足够时间是给 IO 的。这里也不需要急着去理解什么 IO 任务、什么非 IO 任务。

然后我们继续走它的构造方法,我们看到上面的构造方法调用了父类的构造器,它的父类是 SingleThreadEventLoop。

protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,

boolean addTaskWakesUp, int maxPendingTasks,

RejectedExecutionHandler rejectedExecutionHandler) {

super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler);

// 我们可以直接忽略这个东西,以后我们也不会再介绍它

tailTasks = newTaskQueue(maxPendingTasks);

}

SingleThreadEventLoop 这个名字很诡异有没有?然后它的构造方法又调用了父类 SingleThreadEventExecutor 的构造方法:

protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,

boolean addTaskWakesUp, int maxPendingTasks,

RejectedExecutionHandler rejectedHandler) {

super(parent);

this.addTaskWakesUp = addTaskWakesUp;

this.maxPendingTasks = Math.max(16, maxPendingTasks);

this.executor = ObjectUtil.checkNotNull(executor, “executor”);

// taskQueue,这个东西很重要,提交给 NioEventLoop 的任务都会进入到这个 taskQueue 中等待被执行

// 这个 queue 的默认容量是 16

taskQueue = newTaskQueue(this.maxPendingTasks);

rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, “rejectedHandler”);

}

到这里就更加诡异了,NioEventLoop 的父类是 SingleThreadEventLoop,而 SingleThreadEventLoop 的父类是 SingleThreadEventExecutor,它的名字告诉我们,它是一个 Executor,是一个线程池,而且是 Single Thread 单线程的。

也就是说,线程池 NioEventLoopGroup 中的每一个线程 NioEventLoop 也可以当做一个线程池来用,只不过池中只有一个线程。这种设计虽然看上去很巧妙,不过有点反人类的样子。

上面这个构造函数比较简单:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

写在最后

学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

最后再分享的一些BATJ等大厂20、21年的面试题,把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

蚂蚁金服三面直击面试官的Redis三连,Redis面试复习大纲在手,不慌

Mybatis面试专题

蚂蚁金服三面直击面试官的Redis三连,Redis面试复习大纲在手,不慌

MySQL面试专题

蚂蚁金服三面直击面试官的Redis三连,Redis面试复习大纲在手,不慌

并发编程面试专题

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
名字告诉我们,它是一个 Executor,是一个线程池,而且是 Single Thread 单线程的。

也就是说,线程池 NioEventLoopGroup 中的每一个线程 NioEventLoop 也可以当做一个线程池来用,只不过池中只有一个线程。这种设计虽然看上去很巧妙,不过有点反人类的样子。

上面这个构造函数比较简单:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-03MWY4ZH-1712512577440)]

[外链图片转存中…(img-zJ7ShPfN-1712512577441)]

[外链图片转存中…(img-iaLw6pUR-1712512577441)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

写在最后

学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

最后再分享的一些BATJ等大厂20、21年的面试题,把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

[外链图片转存中…(img-tS6dFeCU-1712512577441)]

Mybatis面试专题

[外链图片转存中…(img-AnHW8nly-1712512577441)]

MySQL面试专题

[外链图片转存中…(img-umVo90YL-1712512577441)]

并发编程面试专题

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值