Netty启动之后, IO线程便处于无限循环执行中.代码如下
// 源码位置: io.netty.channel.nio.NioEventLoop#run
@Override
protected void run() {
int selectCnt = 0;
for (;;) {
try {
int strategy;
try {
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
// 1.轮询IO事件
strategy = select(curDeadlineNanos);
} catch (IOException e) {
...
}
...
if (ioRatio == 100) {
...
} else if (strategy > 0) {
final long ioStartTime = System.nanoTime();
try {
// 2.处理IO事件
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
// 3.执行任务
ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
...
}
}
}
这里关注一下第3点执行任务
/**
* Poll all tasks from the task queue and run them via {@link Runnable#run()} method. This method stops running the tasks in the task queue and returns if it ran longer than {@code timeoutNanos}.
*/
protected boolean runAllTasks(long timeoutNanos) {
fetchFromScheduledTaskQueue();
Runnable task = pollTask();
if (task == null) {
afterRunningAllTasks();
return false;
}
final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
safeExecute(task);
runTasks ++;
// 每64个任务检查一次超时,因为nanoTime()比较昂贵.
// Check timeout every 64 tasks because nanoTime() is relatively expensive.
// XXX: Hard-coded value - will make it configurable if it is really a problem.
if ((runTasks & 0x3F) == 0) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}
入参timeoutNanos设置执行任务的超时时间. 一旦超过这个设定的时间,则停止执行任务.
按照一般的思路,每执行一个任务之后,就要调用获取时间,然后判断一下时间是否到达入参设置的超时时间,如果未到达,则继续执行下一个任务, 如果到达,则停止执行任务.
但是Netty并未这样实现. 它是每执行64个任务之后,在调用获取时间,判断是否到达设置的超时时间. 之所以这样做, 源码中也给了注释解释,因为调用获取时间是消耗性能的,应该减少调用获取时间的次数.
那我们看一下java.lang.System#nanoTime源码.
// 源码位置: java.lang.System#nanoTime
public static native long nanoTime();
是一个native方法, 继续查看JVM中的源码.
// 源码位置: hotspot/src/share/vm/prims/jvm.cpp
JVM_LEAF(jlong, JVM_NanoTime(JNIEnv *env, jclass ignored))
JVMWrapper("JVM_NanoTime");
return os::javaTimeNanos();
JVM_END
// 源码位置: hotspot/src/os/linux/vm/os_linux.cpp
jlong os::javaTimeNanos() {
if (Linux::supports_monotonic_clock()) {
struct timespec tp;
// 系统调用
int status = Linux::clock_gettime(CLOCK_MONOTONIC, &tp);
assert(status == 0, "gettime error");
jlong result = jlong(tp.tv_sec) * (1000 * 1000 * 1000) + jlong(tp.tv_nsec);
return result;
} else {
timeval time;
// 调用C库函数,底层调用系统调用
int status = gettimeofday(&time, NULL);
assert(status != -1, "linux error");
jlong usecs = jlong(time.tv_sec) * (1000 * 1000) + jlong(time.tv_usec);
return 1000 * usecs;
}
}
获取时间需要进行系统调用,进行用户态和内核态切换.
我们可以写一个小测试案例,验证下
public class Example {
public static void main(String[] args) {
System.out.println("start...");
long l = System.nanoTime();
System.out.println(l);
System.out.println("end...");
}
}
编译
javac Example.java
使用strace命令查看系统调用
[v-dev: tmp]# strace -ff -o out java Example
start...
35737962287000
end...
部分系统调用输出如下
write(1, "start...", 8) = 8
gettimeofday({tv_sec=1638243469, tv_usec=142774}, NULL) = 0
write(1, "\n", 1) = 1
clock_gettime(CLOCK_MONOTONIC, {tv_sec=35737, tv_nsec=962287000}) = 0
gettimeofday({tv_sec=1638243469, tv_usec=143295}, NULL) = 0
gettimeofday({tv_sec=1638243469, tv_usec=143447}, NULL) = 0
gettimeofday({tv_sec=1638243469, tv_usec=143568}, NULL) = 0
write(1, "35737962287000", 14) = 14
write(1, "\n", 1) = 1
write(1, "end...", 6) = 6
write(1, "\n", 1) = 1
可以看到它调用了clock_gettime系统调用.
综上, Netty为了提高任务执行的性能,减少系统调用次数,每执行64个任务之后,才会调用系统调用,获取时间,判断是否超时,如果到达了超时时间则停止执行任务,否则继续执行任务.
再说一点
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.10</version>
</dependency>
糊涂(Hutool)工具中的雪花算法,在获取时间的时候,并没有按照如下实现
private long genTime() {
return System.currentTimeMillis();
}
而是使用了下面的代码
private long genTime() {
return this.useSystemClock ? SystemClock.now() : System.currentTimeMillis();
}
作者使用了一个SystemClock类
作者进行了说明. 由于System.currentTimeMillis()会进行系统调用(这个我们在上面已经验证过了), 进行用户态和内核态切换,比较耗时.
这里作者使用一个SystemClock类.
在实例化SystemClock类的时候,底层会启动一个线程, 周期性的获取系统时间. 默认1毫秒获取一次. 作者这样的优化,也是为了提高性能.
综上, Netty和Hutool为了提高性能, 在获取时间的地方, 采取了对应的策略应对.