创建
NioEventLoop的创建主要做了如下几件事情:
- 创建TheadPerTaskExecutor, TPTE的作用就是根据线程工厂创建新的线程。
- 创建一组EventExecutor,也就是EventLoop。
- EventLoop初始化,创建两个队列tailTasks、taskQueue。taskQueue用来执行任务,tailTasks用来进行收尾工作。
- 创建优化后的多路复用器。JDK的多路复用器存储SelectionKey用的是HashSet,优化后的使用数组。
- 创建EventLoop选择器,用于轮训选择EventLoop。
启动
NioEventLoop的启动时机有两个:
- 服务端bind
- 新连接接入
以服务端绑定为例
- 创建channel。
- next()是通过线程选择器选择一个NioEventLoop。
- 将NioEventLoop和这个channle绑定起来,下次这个channel直接使用这个将NioEventLoop。
- 执行NioEventLoop的execute方法,提交注册任务
- 判读这个线程是不是EventLoop线程
- 将任务添加到taskQueue
- 启动线程
- CAS判断NioEventLoop的状态
- 使用NioEventLoop的ThreadPerTaskExecutor执行任务
- 将ThreadPerTaskExecutor创建的当前线程赋值NioEventLoop的thread属性
- 执行run方法
EventLoop实现串行执行的关键
这里有几个非常关键的点也是NioEventLoop实现串行化执行的关键点。
EventLoop定义了一个state属性,分别有以下几个状态
private static final int ST_NOT_STARTED = 1;
private static final int ST_STARTED = 2;
private static final int ST_SHUTTING_DOWN = 3;
private static final int ST_SHUTDOWN = 4;
private static final int ST_TERMINATED = 5;
在启动Eventloop的时候借助了JDK的AtomicIntegerFiledUpdater,原子化更新state,无锁化保证在高并发情况下,EventLoop只启动一次。启动的时候会把当前的线程赋值到thread属性。
private static final AtomicIntegerFieldUpdater<SingleThreadEventExecutor> STATE_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(SingleThreadEventExecutor.class, "state");
private void startThread() {
if (state == ST_NOT_STARTED) {
if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
try {
doStartThread();
} catch (Throwable cause) {
STATE_UPDATER.set(this, ST_NOT_STARTED);
PlatformDependent.throwException(cause);
}
}
}
}
private void doStartThread() {
assert thread == null;
executor.execute(new Runnable() {
@Override
public void run() {
// 赋值
thread = Thread.currentThread();
if (interrupted) {
thread.interrupt();
}
}
}
}
所以使用EventLoop的时候,会先用isEventLoop
判断当前线程是不是EventLoop线程,是的话直接执行,不是的话执行启动流程。
所以标准使用EventLoop的写法是:
@Test
public void event_loop_test() {
DefaultEventLoop eventLoop = new DefaultEventLoop();
boolean inEventLoop = eventLoop.inEventLoop();
if (inEventLoop) {
doSomething();
} else {
eventLoop.execute(() -> {
doSomething();
});
}
}
执行
NioEventLoop的run方法主要做了三件事情:
- 调用 select()从操作系统中轮询到网络 IO 事件。
- 处理 IO 事件。
- 处理 nioEventLoop 的任务队列中的普通任务和定时任务。
因为NioEventLoop即需要处理IO事件又需要处理普通任务和定时任务,所以NioEventLoop优先处理taskQueue中的任务。因为NioEventLoop只有一个线程处理任务,如果普通任务耗时比较多,将会阻塞,导致IO事件无法被处理。
通过ioRatio
可以调整IO时间和普通任务时间的占比。
如何规避JDK的空轮训BUG
for (; ; ) {
// ...
阻塞执行轮训
int selectedKeys = selector.select(timeoutMillis);
selectCnt ++;
// 省略其他和空轮询无关的代码
long time = System.nanoTime();
// 下面这一行代码等价于: time - currentTimeNanos >= TimeUnit.MILLISECONDS.toNanos(timeoutMillis)
// 什么意思呢?当前时间 - 运行之前的时间,如果大于 select阻塞的时间,这就说明进行了select的阻塞操作,没有进行JDK的空轮询
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// 如果selectCnt的值大于等于512(这个数值可配置),说明系统可能进行了空轮训,因此创建新的selector,并用新的selector来替换之前的selector
selector = selectRebuildSelector(selectCnt);
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
首先Netty会计算多路复用器调用select的时间,每调用一次多路复用器的select,selectCnt的值就加1。
如果(当前时间>=阻塞时间+运行时间)证明没有发生空轮训,selectCnt设置为1。如果发生了空轮训,select没有阻塞,就不会(当前时间>=阻塞时间+运行时间),那么就创建一个新的多路复用器,替换当前的掉当前的多路复用器。