Selector
选择器是 Java 多路复用模型的一个实现,可以同时监控多个非阻塞套接字通道。示意图大致如下:

创建选择器
选择器 Selector 是一个抽象类,所以不能直接创建。Selector 提供了一个 open 方法,通过 open 方法既可以创建选择器实例。示例代码如下:
Selector selector = Selector.open();
Java 选择器是对底层多路复用接口的一个包装,这里的 open 方法也不例外。假设我们的 Java 运行在 Linux 平台下,那么 open 最终所做的事情应该是调用操作系统的epoll_create函数,用于创建 epoll 实例。真实情况是不是如此呢?答案就在冰山深处,接下来就让我们一起去求索吧。下面我们将沿着 open 方法一路走下去,如下:
public abstract class Selector implements Closeable { public static Selector open() throws IOException { // 创建 SelectorProvider,再通过其 openSelector 方法创建 Selector return SelectorProvider.provider().openSelector(); } // 省略无关代码 } public abstract class SelectorProvider {
private static final Object lock = new Object();
private static SelectorProvider provider = null;
public static SelectorProvider provider() { synchronized (lock) { if (provider != null) return provider; return AccessController.doPrivileged( new PrivilegedAction<SelectorProvider>() { public SelectorProvider run() { if (loadProviderFromProperty()) return provider; if (loadProviderAsService()) return provider; // 创建默认的 SelectorProvider provider = sun.nio.ch.DefaultSelectorProvider.create(); return provider; } }); } } } public class DefaultSelectorProvider { private DefaultSelectorProvider() { } /** * 根据系统名称创建相应的 SelectorProvider */ public static SelectorProvider create() { String osname = AccessController .doPrivileged(new GetPropertyAction("os.name")); if (osname.equals("SunOS")) return createProvider("sun.nio.ch.DevPollSelectorProvider"); if (osname.equals("Linux")) return createProvider("sun.nio.ch.EPollSelectorProvider"); // return new sun.nio.ch.PollSelectorProvider(); } /** * 加载 SelectorProvider 类,并创建实例 */ @SuppressWarnings("unchecked") private static SelectorProvider createProvider(String cn) { Class<SelectorProvider> c; try { c = (Class<SelectorProvider>)Class.forName(cn); } catch (ClassNotFoundException x) { throw new AssertionError(x); } try { return c.newInstance(); } catch (IllegalAccessException | InstantiationException x) { throw new AssertionError(x); } } } /** * 创建完 SelectorProvider,接下来要调用 openSelector 方法 * 创建 Selector 的继承类了。 */ public class EPollSelectorProvider extends SelectorProviderImpl { public AbstractSelector openSelector() throws IOException { return new EPollSelectorImpl(this); } } class EPollSelectorImpl extends SelectorImpl { EPollSelectorImpl(SelectorProvider sp) throws IOException { // 调用父类构造方法 super(sp); long pipeFds = IOUtil.makePipe(false); fd0 = (int) (pipeFds >>> 32); fd1 = (int) pipeFds; // 创建 EPollArrayWrapper,EPollArrayWrapper 是一个重要的实现 pollWrapper = new EPollArrayWrapper(); pollWrapper.initInterrupt(fd0, fd1); fdToKey = new HashMap<>(); } } public abstract class SelectorImpl extends AbstractSelector { protected SelectorImpl(SelectorProvider sp) { super(sp); keys = new HashSet<SelectionKey>(); selectedKeys = new HashSet<SelectionKey>(); /* 初始化 publicKeys 和 publicSelectedKeys, * publicKeys 即 selector.keys() 方法所返回的集合, * publicSelectedKeys 则是 selector.selectedKeys() 方法返回的集合 */ if (Util.atBugLevel("1.4")) { publicKeys = keys; publicSelectedKeys = selectedKeys; } else { publicKeys = Collections.unmodifiableSet(keys); publicSelectedKeys = Util.ungrowableSet(selectedKeys); } } } /** * EPollArrayWrapper 一个重要的实现,这一层再往下就是 C 代码了 */ class EPollArrayWrapper { EPollArrayWrapper() throws IOException { // 调用 epollCreate 方法创建 epoll 文件描述符 epfd = epollCreate(); // the epoll_event array passed to epoll_wait // 初始化 pollArray,该对象用于存储就绪文件描述符和事件 int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT; pollArray = new AllocatedNativeObject(allocationSize, true); pollArrayAddress = pollArray.address(); // eventHigh needed when using file descriptors > 64k if (OPEN_MAX > MAX_UPDATE_ARRAY_SIZE) eventsHigh = new HashMap<>(); } // epollCreate 方法是 native 类型的 private native int epollCreate(); }
以上代码时 Java 层面的,Java 层调用栈最下面的类是 EPollArrayWrapper(源码路径可以在附录中查找)。EPollArrayWrapper 是一个重要的实现,起着承上启下的作用。上层是 Java 代码,下层是 C 代码。上层的代码看完了,接下来看看冰山深处的 C 代码:
JNIEXPORT jint JNICALL Java_sun_nio_ch_EPollArrayWrapper_epollCreate(JNIEnv *env, jobject this) { // 调用 epoll_create 函数创建 epoll 实例,并返回文件描述符 epfd int epfd = epoll_create(256); if (epfd < 0) { JNU_ThrowIOExceptionWithLastError(env, "epoll_create failed"); } return epfd; }
仅做了创建 epoll 实例这一件事。看到这里,答案就明了了。最后在附一张时序图帮助大家理清代码调用顺序,如下:

选择键 SelectionKey 包含4种事件,分别是:
public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4;
事件之间可以通过或运算进行组合,比如:
int interestOps = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
interestOps 和 readyOps
interestOps 即感兴趣的事件集合,通道调用 register 方法注册时会设置此值,interestOps 可通过 SelectionKey interestOps() 方法获取。readyOps 是就绪事件集合,可通过 SelectionKey readyOps() 获取。
interestOps 和 readyOps 被声明在 SelectionKey 子类 SelectionKeyImpl 中,代码如下:
public class SelectionKeyImpl extends AbstractSelectionKey { private volatile int interestOps; private int readyOps; }
接下来再来看看与 readyOps 事件集合相关的几个方法,如下:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
接下来以 isReadable 方法为例,简单看一下这个方法是如何实现。
public final boolean isReadable() { return (readyOps() & OP_READ) != 0; }
可以通过或运算组合事件,这里则是通过与运算来测试某个事件是否在事件集合中。比如
readyOps = SelectionKey.OP_READ | SelectionKey.OP_WRITE = 0101, readyOps & OP_READ = 0101 & 0001 = 0001, readyOps & OP_CONNECT = 0101 & 1000 = 0
readyOps & OP_READ != 0,所以 OP_READ 在事件集合中。readyOps & OP_CONNECT == 0,所以 OP_CONNECT 不在事件集合中。
attach 方法
attach 是一个好用的方法,通过这个方法,可以将对象暂存在 SelectionKey 中,待需要的时候直接取出来即可。比如本文对应的练习代码实现了一个简单的 HTTP 服务器,在读取用户请求数据后(即 selectionKey.isReadable() 为 true),会去解析请求头,然后将请求头信息通过 attach 方法放入 selectionKey 中。待通道可写后,再从 selectionKey 中取出请求头,并根据请求头回复客户端不同的消息。当然,这只是一个应用场景
selectionKey.attach(obj);
Object attachedObj = selectionKey.attachment();
通道注册
通道注册即将感兴趣的事件告知 Selector,待事件发生时,Selector 即可返回就绪事件,就可以去做后续的事情了。比如 ServerSocketChannel 通道通常对 OP_ACCEPT 事件感兴趣,那么我们就可以把这个事件注册给 Selector。待事件发生,即服务端接受客户端连接后,我们即可获取这个就绪的事件并做相应的操作。通道注册的示例代码如下:
channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
选择方法
Selector 包含3种不同功能的选择方法,分别如下:
- int select()
- int select(long timeout)
- int selectNow()
select() 是一个阻塞方法,仅在至少一个通道处于就绪状态时才返回。
select(long timeout) 同样也是阻塞方法,不过可对该方法设置超时时间(timeout > 0),使得线程不会被一直阻塞。如果 timeout = 0,会一直阻塞线程。
selectNow() 为非阻塞方法,调用后立即返回。
以上3个方法均返回 int 类型值,表示每次调用 select 或 selectNow 方法后,新就绪通道的数量。如果某个通道在上一次调用 select 方法时就已经处于就绪状态,但并未将该通道对应的 SelectionKey 对象从 selectedKeys 集合中移除。假设另一个的通道在本次调用 select 期间处于就绪状态,此时,select 返回1,而不是2。
2.4.2 选择过程
选择方法用起来虽然简单,但方法之下隐藏的逻辑还是比较复杂的。大致分为下面几个步骤:
- 检查已取消键集合 cancelledKeys 是否为空,不为空则将 cancelledKeys 的键从 keys 和 selectedKeys 中移除,并将键和通道注销。
- 调用操作系统的 epoll_ctl 函数将通道感兴趣的事件注册到 epoll 实例中
- 调用操作系统的 epoll_wait 函数监听事件
- 再次执行步骤1
- 更新 selectedKeys 集合,并返回就绪通道数量
上面五个步骤对应于 EPollSelectorImpl 类中 doSelect 方法的逻辑,如下:
+----EPollArrayWrapper.java int poll(long timeout) throws IOException { // 调用 epoll_ctl 函数注册事件,对应步骤3 updateRegistrations(); // 调用 epoll_wait 函数等待事件发生,对应步骤4 updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd); for (int i=0; i<updated; i++) { if (getDescriptor(i) == incomingInterruptFD) { interruptedIndex = i; interrupted = true; break; } } return updated; } /** * Update the pending registrations. */ private void updateRegistrations() { synchronized (updateLock) { int j = 0; while (j < updateCount) { // 获取 fd 和 events,这两个值在调用 register 方法时被存储到数组中 int fd = updateDescriptors[j]; short events = getUpdateEvents(fd); boolean isRegistered = registered.get(fd); int opcode = 0; if (events != KILLED) { // 确定 opcode 的值 if (isRegistered) { opcode = (events != 0) ? EPOLL_CTL_MOD : EPOLL_CTL_DEL; } else { opcode = (events != 0) ? EPOLL_CTL_ADD : 0; } if (opcode != 0) { // 注册事件 epollCtl(epfd, opcode, fd, events); // 设置 fd 的注册状态 if (opcode == EPOLL_CTL_ADD) { registered.set(fd); } else if (opcode == EPOLL_CTL_DEL) { registered.clear(fd); } } } j++; } updateCount = 0; } // 下面两个均是 native 方法 private native void epollCtl(int epfd, int opcode, int fd, int events); private native int epollWait(long pollAddress, int numfds, long timeout, int epfd) throws IOException; }
参考:
http://www.tianxiaobo.com/2018/04/03/Java-NIO%E4%B9%8B%E9%80%89%E6%8B%A9%E5%99%A8/
浙公网安备 33010602011771号