在之前的文章中我们讨论过NioEventLoop创建过程.
创建的第一个步骤就是创建线程执行器ThreadPerTaskExecutor, 这个线程执行器就是用来创建Netty底层的线程的. 在学习Java的Thread时候,线程默认名称类似thread-0,thread-1,thread-2...以此类推. 而线程的名称对于我们排查问题的时候也是起到很大作用的, 因此我们在设计线程池, 也会根据一定的规则给线程池中的线程命名, 这也是一个好的习惯.
在Netty中自然也会给线程池中的线程命名, 接下来我们就分析下它的命名规则.
上面的图中有两个线程池,一个叫bossGroup,另一个叫workerGroup. 它们都属于EventLoopGroup类型. 前面我们也提高过, bossGroup负责接收客户端请求, workerGroup犹如其名一样, 是个'工人',负责处理客户端的IO读写操作的.
在这两个Group内部有很多个NioEventLoop
如果我们在创建EventLoopGroup时, 没有传线程数量, 那么每个线程池默认创建2*CPU个线程.
每个线程的命名规则: nioEventLoop-n-n, 例如nioEventLoop-2-1
接下来我们解释下后面两个数字如何确定的.
我们就拿nioEventLoop-2-1这个为例
备注: 包括dubbo, RocketMQ这样的框架在内, 它们底层在使用Netty时的代码类似, 如下
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
上面的代码主线程main从上往下执行的时候, 第一个bossGroup是第1个线程池, 第二个workerGroup是第2个线程池. 依次类推, 如果代码里new了5个NioEventLoopGroup, 那么第五个group就是第5个线程池.
因此我们示例中的nioEventLoop-2-1的数字2就表示第2个线程池的意思. 也就是nioEventLoop-2-1这个名字的线程是在第2个线程池中的.
我们继续分析nioEventLoop-2-1中数字1的由来.
每个EventLoopGroup中有多个NioEventLoop. 当NioEventLoop在启动的时候会创建底层的线程.根据选择器EventExecutorChooser, 从线程池中第一个被选择出来为客户端提供服务的NioEventLoop就是第1个线程, 从线程池中第二个被选择出来为客户端提供服务的NioEventLoop就是第2个线程, 以此类推. 所以示例nioEventLoop-2-1中的数字1就是表示线程池中的第1个线程, 整体就表示第2个线程池中的第1个线程.
备注: 示例nioEventLoop-2-1中的nioEventLoop这个名字是固定的.
实战
接下来我们从实际去看下它们的名字
服务端代码如下
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
public class Server {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ChannelPipeline channelPipeline = ch.pipeline();
// ...
}
});
// 绑定端口 同步等待成功
ChannelFuture channelFuture1 = serverBootstrap.bind("127.0.0.1", 8080).sync();
// 等待服务端监听端口关闭
channelFuture1.channel().closeFuture().sync();
} finally {
// 执行到此处说明服务端已经关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
我们把上面的代码启动, 然后通过telnet 127.0.0.1 8080方式连接服务器. 我们使用JDK自带的jvisualvm查看线程.
通过telnet连接
我们发现多了两个线程, 因为我们通过telnet连接了两次, 所以多了两个线程. 其中第二个数字一个是-1, 另一个是-2,表示第1个和第2个线程的意思.
但是
根据上面的服务端代码和前面的讲解, 我们明明创建了两个线程池, 那么第一个数字应该是-1和-2才对, 可是我们实际观察发现, 却是-2和-3. (更准确的说, nioEventLoopGroup-2表示bossGroup, nioEventLoopGroup-3表示workerGroup). 我们的代码明明只是new出来2个NioEventLoopGroup, 现在实际观察却发现nioEventLoopGroup-1被别人占了.
我们从源码中寻找答案
当我们在代码中通过new实例化NioEventLoopGroup时, 由于NioEventLoopGroup继承MultithreadEventExecutorGroup, 所以这个MultithreadEventExecutorGroup也会被实例化.
从图中我们发现, 会实例化一个DefaultPromise, 其中有个GlobalEventExecutor.INSTANCE. 使用单例模式创建GlobalEventExecutor. 其中GlobalEventExecutor有个属性
final ThreadFactory threadFactory = new DefaultThreadFactory(DefaultThreadFactory.toPoolName(getClass()), false, Thread.NORM_PRIORITY, null);
再跟踪下DefaultThreadFactory
我们看右下角发现了真相, -1被globalEventExecutor-1-使用了.
备注: DefaultThreadFactory这个工厂类在创建bossGroup和workerGroup都会被使用.
关于GlobalEventExecutor的作用以及既然创建出来了为啥我们在上面的线程中又没有观察到这个线程呢? 我们下次再聊.