Netty源码分析系列之NioEventLoop

文章目录

    • NioEventLoopGroup源码分析
      • 三种Reactor线程模型
        • Reactor单线程模型
        • Reactor多线程模型
        • 主从Reactor多线程模型
        • Netty线程模型
      • NioEventLoop源码分析
      • NioEventLoop创建
        • NioEventLoopGroup构造方法
        • MultithreadEventExecutorGroup 构造方法
        • 创建NioEventLoop对象
        • 创建Selector对象
      • NioEventLoop对象启动

在之前创建netty服务端和客户端的例子中第一步就是创建对应的NioEventLoopGroup并添加到BootStrap中用于处理io读写和客户端连接操作。
这里的NioEventLoopGroup和NioEventLoop是netty网络IO的线程模型,在分析其源码之前我们先了解一下Netty的线程模型

NioEventLoopGroup源码分析

​ NioEventLoopGroup相关类是netty进行TCP网络通信的线程模型,通过其UML类图关系可以看出其本质上是一个线程池对象(继承ScheduledExecutorService接口除了具备线程池特性外,其可以执行定时或者延时任务),可以通过设置相关线程数采用不同形式的Reactor线程模型。

三种Reactor线程模型

Reactor单线程模型

Netty源码分析系列之NioEventLoop_第1张图片

​```
/**
 * Reactor单线程模型
 */ 
public void aloneReactor() {
   //创建一个线程数为1的NioEventLoopGroup对象 该对象本质上是线程池对象
   //从而模拟出Reactor单线程模型
   NioEventLoopGroup aloneEventGroup = new NioEventLoopGroup(1);
   //NIO服务启动辅助类
   ServerBootstrap bootstrap = new ServerBootstrap();
   //添加线程池组
   bootstrap.group(aloneEventGroup);
   //省略部分代码
}

该线程模型只适用于小容量 低并发场景,如果在请求量变大 则一个线程要处理成百上千链路 cpu负荷过重从而导致处理性能变慢或者阻塞(导致某些读写、连接操作超时),而且单线程可靠性无法保证。

Reactor多线程模型

Netty源码分析系列之NioEventLoop_第2张图片

/**
 * Reactor多线程模型
 */
public void multiReactor() {
    //创建一个线程数为1的NioEventLoopGroup对象 用于单独处理客户端连接操作
    NioEventLoopGroup acceptEventGroup = new NioEventLoopGroup(1);
    //一旦建立连接 IO读写操作交给该线程组去进行处理
    NioEventLoopGroup nioReadAndWriteEventGroup = new NioEventLoopGroup();

    //NIO服务启动辅助类
    ServerBootstrap bootstrap = new ServerBootstrap();
    //添加线程池组
    bootstrap.group(acceptEventGroup,nioReadAndWriteEventGroup);
    //省略部分代码
}

针对Reactor单线程模型的性能瓶颈,Reactor多线程模型使用一个线程单独处理客户端连接
连接建立后,读写请求交个一个线程组去处理,这种线程模型在绝大数应用场景下都能满足性能要求,但是如果并发达到百万级别,且连接请求有复杂的安全认证等功能则会出现性能问题。
所以此处衍生出主从 Reactor多线程模型.

主从Reactor多线程模型

Netty源码分析系列之NioEventLoop_第3张图片

/**
 * 主从Reactor多线程模型
 */
public void masterSlaveMultiReactor() {
    //创建一个NioEventLoopGroup 线程对象 用于百万并发下处理客户端连接操作
    NioEventLoopGroup acceptEventGroup = new NioEventLoopGroup();
    //一旦建立连接 IO读写操作交给该线程组去进行处理
    NioEventLoopGroup nioReadAndWriteEventGroup = new NioEventLoopGroup();
    //NIO服务启动辅助类
    ServerBootstrap bootstrap = new ServerBootstrap();
    //添加线程池组
    bootstrap.group(acceptEventGroup,nioReadAndWriteEventGroup);
    //省略部分代码
}

该线程模型提供两个线程组 一个线程组用来处理客户端连接认证等复杂处理(避免了之前单线程处理连接的问题)读写IO操作交由另外一个线程组进行处理。该模型解决了绝大多数业务场景的性能问题。

Netty线程模型

Netty源码分析系列之NioEventLoop_第4张图片

NioEventLoop源码分析

​ 因为netty的线程模型封装的比较好,好多类并没有实际功能只是提供构造方法,这里我们从代码使用过程去分析其源码,其他边边角角。

NioEventLoopGroup 从功能上,可以把它当做一个线程池来理解,它管理一个包含NioEventLoop的childern列表。

NioEventLoop 从功能上,可以把它当做一个线程来理解,当它启动以后,它就会不停地循环处理三种任务(从类名上也能体现出循环处理的思想:Loop)。这三种任务分别是哪三种任务呢?

  1. 网络 IO 事件;
  2. 普通任务。通过调用execute(Runnable task) 来执行普通任务。
  3. 定时任务。通过调用schedule(Runnable task,long delay,TimeUnit unit) 来执行定时任务。

Netty源码分析系列之NioEventLoop_第5张图片

下面我们从NioEventLoop创建和使用两个角度对源码进行分析。

NioEventLoop创建

​ NioEventLoop的创建是通过先创建一个NioEventLoopGroup,在由其构建NioEventLoop对象,根据其UML类图其主要核心属性功能在父SingleThreadEventExecutor类中

NioEventLoopGroup构造方法

    public NioEventLoopGroup(int nThreads, Executor executor) {
        //第三个参数为对应的NIO的多路复用器 该多路复用器实现是基于操作系统的
        //不同的操作系统 不同的多路复用器实现 逻辑比较简单:
        //先通过loadProviderFromProperty方法 查找 java.nio.channels.spi.SelectorProvider属性对应的SelectorProvider class 并反射实例化
        //没有的话 调用loadProviderAsService方法 通过SPI的机制获取服务实例
        //还没有默认创建一个WindowsSelectorProvider实现
        this(nThreads, executor, SelectorProvider.provider());
}
    protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
        //线程个数如果是0 则默认创建当前服务器CPU核数的两倍。
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
    }

  • nThreads:要创建的线程的数量,如果前面使用的是 NioEventLoopGroup 无参构造器,此时 nThreads 的值为 0,如果使用的是 NioEventLoopGroup 的有参构造方法,nThreads 的值为构造方法中传入的值。
  • executor:线程执行器,默认是 null,这个属性的值会在后面创建 NioEventLoop 时,进行初始化。用户可以自定义实现 executor,如果用户自定义了,那么此时 executor 就不为 null,后面就不会再进行初始化。
  • selectorProvider:SelectorProvider 类型,它是通过SelectorProvider.provider() 创建出来的,这是 JDK 中 NIO 相关的 API,会创建出一个 SelectorProvider 对象,这个对象的作用就是创建多路复用器 Selector 和服务端 channel。

MultithreadEventExecutorGroup 构造方法

protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
    if (nThreads <= 0) {
        throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
    }
    /**
     * 1、创建线程执行器:ThreadPerTaskExecutor
     * newDefaultThreadFactory()会创建一个线程工厂,该线程工厂的作用就是用来创建线程,
     * 同时给线程设置名称:nioEventLoop-1-XX
     */
    if (executor == null) {
        executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
    }
    //根据设置的线程数创建对应数量的NioEventLoop
    children = new EventExecutor[nThreads];
    for (int i = 0; i < nThreads; i ++) {
        //创建异常标识
        boolean success = false;
        try {
            //2、创建NioEventLoop对象保存到children数组中
            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 {
            //异常处理
            //如果在创建某个NioEventLoop的时候异常 则之前创建成功的对象关闭
        }
    }

    //为所有创建成功的NioEventLoop 添加执行中断的监听
    final FutureListener<Object> terminationListener = new FutureListener<Object>() {
        @Override
        public void operationComplete(Future<Object> future) throws Exception {
            if (terminatedChildren.incrementAndGet() == children.length) {
                terminationFuture.setSuccess(null);
            }
        }
    };

    for (EventExecutor e: children) {
        e.terminationFuture().addListener(terminationListener);
    }
    //同时创建只读视图集合
    Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
    Collections.addAll(childrenSet, children);
    readonlyChildren = Collections.unmodifiableSet(childrenSet);
}

上述方法有两处核心逻辑

  • 创建创建线程执行器,在调用ThreadPerTaskExecutor的构造方法之前,先通过new DefaultThreadFactory() 创建了一个线程工厂,该线程工厂是DefaultThreadFactory类型,它实现了 ThreadFactory 接口,它的作用就是:当调用 threadFactory 的 newThread()方法时,就会创建出一个线程,同时给线程取一个有意义的名称,名称生成规则为:nioEventLoop-xx-xx。第一个 xx 的含义表示的 NiEventLoopGroup 的组号,在 netty 中可能同时创建 bossGroup 和 workerGroup 两个线程组,所以第一个 xx 表示线程组的序号。第二个 xx 表示的是线程在线程组中的序号。如:nioEventLoop-1-1 表示的是该线程是第一个 NioEventLoopGroup 线程组的第一个线程。ThreadPerTaskExecutor对象的execute()方法会创建一个线程(该方法只会调用一次)
  • 创建NioEventLoop对象。会根据设置的线程数创建相应数量的NioEventLoop对象 用于进行线程调度

创建NioEventLoop对象

​ rebuild()方法根据传入的Executor和SelectorProvider去创建NioEventLoop对象

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider) {
    //调用父类构造器SingleThreadEventExecutor 进行相应属性赋值
    //第三个参数 为addTaskWakesUp(为false 则该线程添加线程任务需要唤醒线程 添加一个空的Runnable任务)
    //该方法还创建类一个LinkedBlockingQueue
    super(parent, executor, false);
    if (selectorProvider == null) {
        throw new NullPointerException("selectorProvider");
    }
    //NioEventLoop需要处理网络IO读写 需要持有一个多路复用器对象
    //则下面的两行代码就是创建多路复用器
    provider = selectorProvider;
    selector = openSelector();
}
  • 调用父类构造器SingleThreadEventExecutor (单线程调度)进行相应的赋值操作,同时会创建taskQueue 任务队列(LinkedBlockingQueue)用于存储线程待执行的任务。
  • 创建多路复用器 NioEventLoop需要处理网络IO读写 需要持有一个多路复用器对象

创建Selector对象

private Selector openSelector() {
    final Selector selector;
    try {
        //直接获取原生的多路复用器
        selector = provider.openSelector();
    } catch (IOException e) {
        throw new ChannelException("failed to open a new selector", e);
    }
    //如果不需要对selector的SelectionKey的hashSet结构进行优化 则直接返回
    //否则执行下面的SelectionKey的结构优化为数组(因为HashSet极端情况下插入操作为O(n)数组添加操作为O(1)
    //该变量可以io.netty.noKeySetOptimization属性进行配置 默认为false
    if (DISABLE_KEYSET_OPTIMIZATION) {
        return selector;
    }

    try {
        //创建SelectedSelectionKeySet 将Selector的selectedKeys和publicSelectedKeys
        //字段使用创建的set填充
        SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
        //省略部分部分代码
    } catch (Throwable t) {
    }

    return selector;
}

​ 直接通过SelectorProvider的openSelector获取原生的Selector,通过变量DISABLE_KEYSET_OPTIMIZATION判断是否要对Selector的

SelectionKey的存储结构进行优化(该变量可以io.netty.noKeySetOptimization属性进行配置 默认为false)不需要优化直接返回,否则

使用数据类型的存储结构以反射的形式将Selector实现类的selectedKeys和publicSelectedKeys替换成SelectedSelectionKeySet。

NioEventLoop对象启动

在分析NioEventLoop创建过程中我们了解到最终DefaultThreadFactory的newThread()方法创建线程对象并包装到NioEventLoop中, 从服务启动的堆栈信息流程如下。

newThread:105, DefaultThreadFactory (io.netty.util.concurrent)
execute:33, ThreadPerTaskExecutor (io.netty.util.concurrent)
doStartThread:783, SingleThreadEventExecutor (io.netty.util.concurrent)
startThread:776, SingleThreadEventExecutor (io.netty.util.concurrent)
execute:654, SingleThreadEventExecutor (io.netty.util.concurrent)
register:400, AbstractChannel$AbstractUnsafe (io.netty.channel)
initAndRegister:276, AbstractBootstrap (io.netty.bootstrap)
doBind:234, AbstractBootstrap (io.netty.bootstrap)   
bind:230, AbstractBootstrap (io.netty.bootstrap)
bind:205, AbstractBootstrap (io.netty.bootstrap)     
start:39, NettyServer (com.xiu.netty.stickyAndUnPack)
main:74, NettyServer (com.xiu.netty.stickyAndUnPack)

​ 在ServerBootstrap的bind()方法中调用io.netty.bootstrap.AbstractBootstrap#doBind()方法,调用io.netty.channel.AbstractChannel.AbstractUnsafe#register()方法

@Override
public final void register(final ChannelPromise promise) {
     // 此时Channel实例化完成 将 eventLoop 和这个 channel绑定了,从此这个 channel 就是有 eventLoop 的了
     // 我觉得这一步其实挺关键的,因为后续该 channel 中的所有异步操作,都要提交给这个 eventLoop 来执行
    
    //如果发起 register 动作的线程就是 eventLoop 实例中的线程,那么直接调用 register0(promise)
    // 对于我们来说,它不会进入到这个分支,
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            //调用EventLoop对象执行 一个Runnable任务(如果没有实例化线程 需要实例化)
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
             //省略异常处理
        }
    }
}

​ NioEventLoop的启动实际上就是其中的线程对象的实例化,该实例化阶段发生在Channel实例化完成之后,绑定端口之前,这时候需要使用线程去调用Runnable任务(第一个任务是register)如果没有实例化线程对象则就去进行实例化操作。

public void execute(Runnable task) {
    if (task == null) {
        throw new NullPointerException("task");
    }
   //判断执行代码线程与该EventLoop是否为同一个线程 如果是进行处理
   //这里是netty的串行无锁设计模式
   //Netty线程间不需要做同步控制,Netty可以通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这样性能就提升了。
    boolean inEventLoop = inEventLoop();
    //线程已经实例化完成 则直接将任务放入taskQueue(之前NioEventLoop创建过程中会新建该队列)等待调度执行
    if (inEventLoop) {
        addTask(task);
    } else {
        //没有实例化先实例化线程 最终调用Executor的execute(ThreadPerTaskExecutor的execute方法)
        startThread();
        //线程实例化完成 则将任务放入待执行队列
        addTask(task);
        //线程关闭 则执行任务拒绝
        if (isShutdown() && removeTask(task)) {
            reject();
        }
    }
    //判断线程阻塞 是否需要唤醒
    //当线程执行IO操作的时候的时候, 如果超时时间timeoutMillis还没有到达的情况下, 
    //IO线程就会处于阻塞状态,此时的IO线程处于阻塞状态,故需要把它唤醒 让IO线程可以及时的处理刚刚非IO线程提交的任务。
    if (!addTaskWakesUp) {
        wakeup(inEventLoop);
    }
}

​ 上述方法主要是针对NioEventLoop中的Thread是否实例化展开,如果实例化完成且代码执行的线程和EventLoop对应的实例化线程一致(netty串行无锁化提升性能),将任务放入线程调度中的待执行队列中。否则需要调用startThread()方法先实例化线程然后将任务放入待执行队列中(涉及到线程关闭的任务拒绝处理),最后判断IO线程阻塞,非IO处理是否需要唤醒线程。

startThread()方法 最终是调用ThreadPerTaskExecutor的execute()实例化线程(该类是NioEventLoop的Executor的线程执行器)调用其中的DefaultThreadFactory类的newThread()方法 创建Thread实例 (代码比较简单 不贴出来)

​ 至此有关NioEventLoop的相关分析到此为止,有不当之处,需要读者指出。

你可能感兴趣的:(netty源码分析,java,netty,网络通信)