引言
select/poll、epoll 这些词汇相信诸位都不陌生,因为在 Redis/Nginx/Netty 等一些高性能技术栈的底层原理中,大家应该都见过它们的身影,接下来重点讲解这块内容,不过在此之前,先上一张图概述 Java-NIO 的整体结构:
观察上述结构,其实 Buffer、Channel 的定义并不算复杂,仅是单纯的三层结构,因此对于源码这块不再去剖析,有兴趣的根据给出的目录结构去调试源码,自然也能摸透其原理实现。
而最关键的是 Selector 选择器,它是整个 NIO 体系中较为复杂的一块内容,同时它也作为 Java-NIO 与内核多路复用模型的“中间者”,但在上述体系中,却出现了之前未曾提及过的 SelectorProvider 系定义,那么它的作用是干嘛的呢?主要目的是用于创建选择器,在Java中创建一般是通过如下方式:
// 创建Selector选择器
Selector selector = Selector.open();
// Selector类 → open()方法
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
复制代码
从源码中可明显得知,选择器最终是由 SelectorProvider 去进行实例化,不过值得一提的是: Selector 的实现是基于工厂模式与 SPI 机制构建的。对于不同 OS 而言,其对应的具体实现并不相同,因此在 Windows 系统下,我们只能观测到 WindowsSelectorXXX 这一系列的实现,而在 Linux 系统时,对于的则是 EPollSelectorXXX 这一系列的实现,所以要牢记的是, Java-NIO 在不同操作系统的环境中,提供了不同的实现 ,如下:
- Windows : select
- Unix : poll
- Mac : kqueue
- Linux : epoll
当然,本次则重点剖析Linux系统下的 select、poll、epoll 的具体实现,对于其他系统而言,原理大致相同。
一、JDK层面的源码入口
简单的对于 Java-NIO 体系有了全面认知后,接下来以 JDK 源码作为入口进行剖析。在Java中,会通过 Selector.select() 方法去监听事件是否被触发,如下:
// 轮询监听选择器上注册的通道是否有事件被触发
while (selector.select() > 0){}
// Selector抽象类 → select()抽象方法
public abstract int select() throws IOException;
// SelectorImpl类 → select()方法
public int select() throws IOException {
return this.select(0L);
}
// SelectorImpl类 → select()完整方法
public int select(long var1) throws IOException {
if (var1 < 0L) {
throw new IllegalArgumentException("Negative timeout");
} else {
return this.lockAndDoSelect(var1 == 0L ? -1L : var1);
}
}
复制代码
当调用 Selector.select() 方法后,最终会调用到 SelectorImpl 类的 select(long var1) 方法,而在该方法中,又会调用 lockAndDoSelect() 方法,如下:
// SelectorImpl类 → lockAndDoSelect()方法
private int lockAndDoSelect(long var1) throws IOException {
// 先获取锁确保线程安全
synchronized(this) {
// 在判断当前选择是否处于开启状态
if (!this.isOpen()) {
// 如果已关闭则抛出异常
throw new ClosedSelectorException();
} else { // 如若处于开启状态
// 获取所有注册在当前选择器上的事件
Set var4 = this.publicKeys;
int var10000;
// 再次加锁
synchronized(this.publicKeys) {
// 获取所有已就绪的事件
Set var5 = this.publicSelectedKeys;
// 再次加锁
synchronized(this.publicSelectedKeys) {
// 真正的调用select逻辑,获取已就绪的事件
var10000 = this.doSelect(var1);
}
}
// 返回就绪事件的数量
return var10000;
}
}
}
复制代码
在该方法中,对于其他逻辑不必太过在意,重点可注意:最终会调用 doSelect() 触发真正的逻辑操作,接下来再看看这个方法:
// SelectorImpl类 → doSelect()方法
protected abstract int doSelect(long var1) throws IOException;
// WindowsSelectorImpl类 → doSelect()方法
protected int doSelect(long var1) throws IOException {
// 先判断一下选择器上是否还有注册的通道
if (this.channelArray == null) {
throw new ClosedSelectorException();
} else { // 如果有的话
// 先获取一下阻塞等待的超时时长
this.timeout = var1;
// 然后将一些取消的事件从选择器上移除
this.processDeregisterQueue();
// 再判断一下是否存在线程中断唤醒
// 这里主要是结合之前的wakeup()方法唤醒阻塞线程的
if (this.interruptTriggered) {
this.resetWakeupSocket();
return 0;
} else { // 如果没有唤醒阻塞线程的需求出现
// 先判断一下辅助线程的数量(守护线程),多则减,少则增
this.adjustThreadsCount();
// 更新一下finishLock.threadsToFinish为辅助线程数
this.finishLock.reset();
// 唤醒所有的辅助线程
this.startLock.startThreads();
try {
// 设置主线程中断的回调函数
this.begin();
try {
// 最终执行真正的poll逻辑,开始拉取事件
this.subSelector.poll();
} catch (IOException var7) {
this.finishLock.setException(var7);
}
// 唤醒并等待所有未执行完的辅助线程完成
if (this.threads.size() > 0) {
this.finishLock.waitForHelperThreads();
}
} finally {
this.end();
}
// 检测状态
this.finishLock.checkForException();
this.processDeregisterQueue();
// 获取当前选择器监听的事件的触发数量
int var3 = this.updateSelectedKeys();
// 本轮poll结束,重置WakeupSocket,为下次执行做准备
this.resetWakeupSocket();
// 最终返回获取到的事件数
return var3;
}
}
}
复制代码
整个过程下来其实也并不短暂,但大体就分为三步:
poll
在这里面,有一个辅助线程的概念,这跟最大文件描述符有关,每当选择器上注册的通道数超过 1023 时,新增一条线程