Netty源码分析-EventLoop(上)

前言

由于整个NioEventLoop类过大,其中还有很多从父类继承的方法。所以在这里通过以问题驱动的方式,对重要部分进行逐步分析。


组成部分

  • Selector

    private Selector selector;
    private Selector unwrappedSelector;
    
  • 线程

    在父类SingleThreadEventLoop的父类SingleThreadEventExecutor中:

    private volatile Thread thread; // EventLoop用来执行任务处理IO事件的线程,单线程
    private final Executor executor; // 执行器,可以理解为单线程的线程池
    

    这两个使用的是同一个线程,executor有更多的功能,比如提交任务,添加定时功能等等。

  • 任务队列

    在父类SingleThreadEventLoop的父类SingleThreadEventExecutor中:

    private final Queue<Runnable> taskQueue; // 存放尚未被处理的任务
    

    在父类SingleThreadEventLoop的父类SingleThreadEventExecutor的父类AbstractScheduledEventExecutor中:

    PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue; // 用来处理定时任务的任务队列
    

Selector在什么时候被创建?

Selector是在NioEventLoop的构造方法中被创建的:

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler, EventLoopTaskQueueFactory queueFactory) {
        super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory), rejectedExecutionHandler);
        this.provider = (SelectorProvider)ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
        this.selectStrategy = (SelectStrategy)ObjectUtil.checkNotNull(strategy, "selectStrategy");
        // 创建Selector
        NioEventLoop.SelectorTuple selectorTuple = this.openSelector();
        this.selector = selectorTuple.selector;
        this.unwrappedSelector = selectorTuple.unwrappedSelector;
    }

在构造方法中调用了openSelector()来创建Selector:

private NioEventLoop.SelectorTuple openSelector() {
        final AbstractSelector unwrappedSelector;
        try {
          // provider.openSelector() 即创建了一个Selector对象
            unwrappedSelector = this.provider.openSelector();
        } catch (IOException var7) {
            throw new ChannelException("failed to open a new selector", var7);
        }
        ...
}

原生的创建Selector的方式为:

public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

由此可见,两者本质上是一样的。


为什么有两个Selector对象?

在上述openSelector()方法中:

。private NioEventLoop.SelectorTuple openSelector() {
        final AbstractSelector unwrappedSelector;
        try {
          // 创建给unwrappedSelector 的Selector是真正的Nio底层的Selector
          unwrappedSelector = this.provider.openSelector();
        } catch (IOException var7) {
            throw new ChannelException("failed to open a new selector", var7);
        }
        ...
}

因为Nio原生的Selector,它的内部有一个SelectionKeys集合,将来发生的事件需要通过集合中获取事件的信息。这个集合默认的实现是Set。Set集合底层是hash表,所以遍历的性能并不高,所以Netty将集合的实现替换成为了数组实现,以此来提升效率。体现源码如下:

Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
                    public Object run() {
                        try {
                          // 通过反射拿到unwrappedSelector中的selectedKeys属性`
                            Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
                            Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
                            if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) {
                                long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField);
                                long publicSelectedKeysFieldOffset = PlatformDependent.objectFieldOffset(publicSelectedKeysField);
                                if (selectedKeysFieldOffset != -1L && publicSelectedKeysFieldOffset != -1L) {
                                    PlatformDependent.putObject(unwrappedSelector, selectedKeysFieldOffset, selectedKeySet);
                                    PlatformDependent.putObject(unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet);
                                    return null;
                                }
                            }
                            // 暴力反射,修改私有属性
                            Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
                            if (cause != null) {
                                return cause;
                            } else {
                                cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
                                if (cause != null) {
                                    return cause;
                                } else {
                                   // 替换为基于数组的selectedKeys实现
                                    selectedKeysField.set(unwrappedSelector, selectedKeySet);
                                    publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
                                    return null;
                                }
                            }
                        } catch (NoSuchFieldException var7) {
                            return var7;
                        } catch (IllegalAccessException var8) {
                            return var8;
                        }
                    }
                });
 selectedKeys = selectedKeySet;
// 调用构造函数,创建unwrappedSelector与selector
return new SelectorTuple(unwrappedSelector,new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));

其实性能并没有提升多少,但是在Netty中这样的优化细节还有很多。所以总体上来看提升的性能还是很可观的。


EventLoop的nio线程在何时启动?

NioEventLoop中的线程,在首次执行任务时,才会被创建,且只会被创建一次。

使用execute执行任务时,会判断此任务是否为懒加载任务:

@Override
public void execute(Runnable task) {
    // 检测传入的任务是否为空,为空会抛出NullPointerException
    ObjectUtil.checkNotNull(task, "task");
    // 执行任务
    // 此处判断了任务是否为懒加载任务,wakesUpForTask的返回值只会为true
    execute(task, !(task instanceof LazyRunnable) && wakesUpForTask(task));
}

execute( ) :

private void execute(Runnable task, boolean immediate) {
    // 判断当前线程是否为NIO线程
    // 判断方法为 return thread == this.thread;
    // this.thread即为NIO线程,首次执行任务时,其为null
    boolean inEventLoop = inEventLoop();
    
    // 向任务队列taskQueue中添加任务
    addTask(task);
    
    // 当前线程不是NIO线程,则进入if语句
    if (!inEventLoop) {
        // 启动NIO线程的核心方法
        startThread();
        
        ...
        
    }
	
    // 有任务需要被执行时,唤醒阻塞的NIO线程
    if (!addTaskWakesUp && immediate) {
        wakeup(inEventLoop);
    }
}

进入startThread方法 :

private void startThread() {
    // 查看NIO线程状态是否为未启动
    // 该if代码块只会执行一次
    // state一开始的值就是ST_NOT_STARTED , 值为1
    // private volatile int state = ST_NOT_STARTED;
    if (state == ST_NOT_STARTED) {
        // 通过原子属性更新器将状态更新为启动(ST_STARTED),值为2
        if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
            boolean success = false;
            try {
                // 执行启动线程
                doStartThread();
                success = true;
            } finally {
                if (!success) {
                    STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);
                }
            }
        }
    }
}

进入doStartThread,真正创建NIO线程并执行任务:

private void doStartThread() {
    assert thread == null;
    // 创建NIO线程并执行任务
    executor.execute(new Runnable() {
        @Override
        public void run() {
            // thread即为NIO线程
            thread = Thread.currentThread();
            if (interrupted) {
                thread.interrupt();
            }

            boolean success = false;
            updateLastExecutionTime();
            try {
                // 执行NioEventLoop内部run方法,其中会不断寻找定时任务、IO事件等任务去实现
                SingleThreadEventExecutor.this.run();
                success = true;
            } 
            
            ...
    });
}

因此,当首次调用execute的时候,nio线程才会启动。并且通过pirvate vloatile int stats = ST_NOT_STARTED;标志位来控制线程只会启动一次。


提交普通任务是否会结束select阻塞?

select()方法中,如果一些条件成立,就会调用:int selectedKeys = selector.select(timeoutMillis);会导致当前线程在没有IO事件的时候一直阻塞下去直到超过设定的超时时间。在Netty中没有使用selector.select();是因为Netty中EventLoop中需要处理多种事件,如果调用此方法则在没有IO事件的时候将一直阻塞下去,从而不能达到处理其他事件任务的目的。

execute()方法中有一个wakeup(boolean inEventLoop)方法,源码部分为:

protected void wakeup(boolean inEventLoop) {
        // inEventLoop 是判断提交任务的线程是不是nio线程,只有不是同一个才会进入代码块, nio线程有自己的唤醒方法不用操心
        // wakenUp是一个原子变量,因为selector.wakeUp()是一个重量级操作应该避免频繁调用,如果有多个线程都来提交任务,
        // 为了避免频繁调用,使用compareAndSet()确定只有一个线程成功可以调用wakeUp()方法
        if (!inEventLoop && wakenUp.compareAndSet(false,true)) {
          // 唤醒nio线程,使其及时进行下一个任务的处理
            this.selector.wakeup();
        }
    }

会,通过wakeup()方法去唤醒select中nio线程

posted @ 2022-02-16 13:30  会编程的老六  阅读(52)  评论(0编辑  收藏  举报