本文转载自Java NIO wakeup实现原理

导语

最近在阅读netty源码时,很好奇Java NIO中Selectorwakeup()方法是如何唤醒selector的,于是决定深扒一下wakeup机制的实现原理,相信对学习NIO是大有裨益的。

wakeup语义

众所周知,selector.select()是阻塞的,通常情况下,只有注册在selector上的channel有事件就绪时,select()才会从阻塞中被唤醒,处理就绪事件。那么,当selector上的channel无就绪事件时,如果想要唤醒阻塞在select()操作上的线程去处理一些别的工作,该如何实现呢?事实上Selector提供了这样的API:

public abstract Selector wakeup();

wakeup()实现的功能:

  • 如果一个线程在调用select()或select(long)方法时被阻塞,调用wakeup()会使线程立即从阻塞中唤醒;如果调用wakeup()期间没有select操作,下次调用select相关操作会立即返回,不会执行poll(),包括调用selectNow()。
  • 在Select期间,多次调用wakeup()与调用一次效果是一样的。

注意:如果调用wakeup()期间没有select操作,后续若先调用一次selectNow(),再次调用select()则会导致阻塞。

wakeup实现机制

以上描述了wakeup()的功能,那么JAVA NIO中是如何实现这个机制的呢?下面以windows环境为例,结合源码来探究这个问题。

通常我们会使用Selector.open()方法创建一个选择器对象,SelectorProvider负责根据不同操作系统来返回不同的实现类,windows平台就返回WindowsSelectorProvider,然后再调用其openSelector()

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

从WindowsSelectorProvider的openSelector()可知,其作用是创建一个WindowsSelectorImpl对象:

public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}

WindowsSelectorImpl就是Selector接口的最终实现类,我们来看看其构造方法都做了什么:

WindowsSelectorImpl(SelectorProvider sp) throws IOException {
super(sp);
pollWrapper = new PollArrayWrapper(INIT_CAP);
wakeupPipe = Pipe.open();
wakeupSourceFd = ((SelChImpl)wakeupPipe.source()).getFDVal(); SinkChannelImpl sink = (SinkChannelImpl)wakeupPipe.sink();
(sink.sc).socket().setTcpNoDelay(true);//禁用Nagle算法
wakeupSinkFd = ((SelChImpl)sink).getFDVal(); pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
}

从源码可知,实例化WindowsSelectorImpl时,会调用Pipe.open()创建一个管道实例wakeupPipe,并从wakeupPipe中获取wakeupSourceFd和wakeupSinkFd两个文件描述符,wakeupSourceFd为read端FD,wakeupSinkFd为write端FD,然后将wakeupSourceFd加入pollWrapper中。

我们知道,pollWrapper的作用是保存当前selector对象上注册的FD,当调用Selector的select()方法时,会将pollWrapper的内存地址传递给内核,由内核负责轮训pollWrapper中的FD,一旦有事件就绪,将事件就绪的FD传递回用户空间,阻塞在select()的线程就会被唤醒。将wakeupSourceFd加入pollWrapper中,表示selector也需要关注wakeupSourceFd上发生的事件,而谁会处理该事件呢?我们先了解下Pipe吧。

从广义上说,管道就是一个用来在两个实体之间单向传输数据的导管。在Unix系统中,管道被用来连接一个进程的输出和另一个进程的输入。Java使用Pipe类实现了一个管道范例,只不过它创建的管道是进程内(JVM进程内部)而非进程间使用的。

Pipe实现的管道由一个可写的SinkChannel和一个可读的SourceChannel组成,这两个Channel的远端是连接起来的,使得一旦将一些字节写入到SinkChannel,就可以在SourceChannel按写入顺序读取这些字节。下面我们看看SinkChannel和SourceChannel类继承结构图:

从图中我们知道几点:

  • SinkChannel和SourceChannel都扩展了AbstractSelectableChannel,因此都支持被注册到一个Selector上;
  • SourceChannel只实现了ReadableByteChannel,因此只支持读操作;同时实现了ScatteringByteChannel,具有将通道中的数据分散到多个缓冲区的能力(矢量I/O);
  • SinkChannel只实现了WritableByteChannel,因此只支持写操作;同时实现了GatheringByteChannel,具有将多个缓冲区的数据聚集到该通道的能力(矢量I/O);
  • SinkChannel和SourceChannel的实现类都实现了SelChImpl,因此都能获取通道相关联的文件描述符FD;
  • SourceChannel和SinkChannel内部通过聚合SocketChannel来完成读和写相关的操作。

下面我们继续分析Pipe.open()的实现;

Pipe.open()最终会创建一个PipeImpl实例:

PipeImpl(final SelectorProvider sp) throws IOException {
try {
AccessController.doPrivileged(new Initializer(sp));
} catch (PrivilegedActionException x) {
throw (IOException)x.getCause();
}
}

PipeImpl构造方法中会创建一个内部类Initializer实例,并调用它的run方法:

public Void run() throws IOException {
LoopbackConnector connector = new LoopbackConnector();
connector.run();
......
}

Initializer的run方法则会创建内部LoopbackConnector的实例,并调用它的run方法,其主要作用是建立一条本地环回连接,看实现:

public void run() {
ServerSocketChannel ssc = null;
SocketChannel sc1 = null;
SocketChannel sc2 = null; try {
// 环回地址
InetAddress lb = InetAddress.getByName("127.0.0.1");
assert(lb.isLoopbackAddress());
InetSocketAddress sa = null;
for(;;) {
// 绑定ServerSocketChannel到环回地址上的一个端口
if (ssc == null || !ssc.isOpen()) {
ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(lb, 0));
sa = new InetSocketAddress(lb, ssc.socket().getLocalPort());
} //建立连接
sc1 = SocketChannel.open(sa);
ByteBuffer bb = ByteBuffer.allocate(8);
long secret = rnd.nextLong();
bb.putLong(secret).flip();
sc1.write(bb); // 获取连接并校验合法性
sc2 = ssc.accept();
bb.clear();
sc2.read(bb);
bb.rewind();
if (bb.getLong() == secret)
break;
sc2.close();
sc1.close();
} // 创建source通道和sink通道
source = new SourceChannelImpl(sp, sc1);
sink = new SinkChannelImpl(sp, sc2);
} catch (IOException e) {
try {
if (sc1 != null)
sc1.close();
if (sc2 != null)
sc2.close();
} catch (IOException e2) {}
ioe = e;
} finally {
try {
if (ssc != null)
ssc.close();
} catch (IOException e2) {}
}
}

run方法完成的功能:

  • 使用本地环回地址“127.0.0.1”创建一个InetAddress实例lb。“127.0.0.1”是一个保留地址,主要用于环回测试,也就是说,目的地址为环回地址的IP数据包永远都不会出现在任何网络中;
  • 创建一个ServerSocketChannelImpl实例ssc,为该通道绑定一个唯一的文件描述符FD;
  • 使用lb和0号端口创建一个InetSocketAddress实例,并将该实例绑定到服务端socket通道上。这里使用了系统预留的0号端口,主要是为了避免写死端口号,操作系统会从动态端口号范围内搜索接下来可以使用的端口号作为服务端socket通道的监听端口;
  • 使用lb和ssc上绑定的端口号创建一个InetSocketAddress实例sa,再用sa创建一个SocketChannel实例,并为该通道绑定一个唯一的文件描述符FD;
  • 客户端socket通道创建成功后会调用connect尝试建立socket连接,由于当前处于阻塞模式,因此connect会阻塞直到成功建立连接或发生IO错误;
  • 创建一个8字节的ByteBuffer对象,填充一个随机long值,然后将缓冲区的数据写入通道sc1;
  • 调用ssc的accept()方法,accept方法会创建一个新的SocketChannel实例,并绑定一个唯一的文件描述符FD,然后使用这个SocketChannel实例读取数据;
  • 比较发送的数据和接收的数据是否相等,若相等,使用sc1创建SourceChannelImpl实例作为管道的source端,使用sc2创建SinkChannelImpl实例作为管道的sink端;
  • 最后调用close()关闭ServerSocketChannel,这样ServerSocketChannel就不会接受新的连接,同时释放绑定在该通道上的FD。

到此,一个管道被成功建立,这个管道的两端为两个通道,SourceChannel作为read端,而SinkChannel为write端,两个通道之间通过TCP进行连接,这样使得在SinkChannel端写入的数据SourceChannel端可以立马读取。

Java中Pipe实现的管道仅用于在同一个Java虚拟机内部传输数据。实际应用中,使用管道在线程间传输数据也是一种不错的方案,它为我们提供了良好的封装性。

现在,回到WindowsSelectorImpl的构造方法中,我们知道,创建一个Selector实例时,还会创建一个管道Pipe实例,并将管道source端wakeupSourceFd加入pollWrapper中,作为第一个注册到Selector的FD,并设置感兴趣的事件为Net.POLLIN,表示对可读事件感兴趣。当Selector在轮训pollWrapper中的FD时,如果wakeupSourceFd发生read事件,那么Selector就会被唤醒,这就是wakeup()的实现原理。看wakeup()实现:

public Selector wakeup() {
synchronized (interruptLock) {
if (!interruptTriggered) {
setWakeupSocket();
interruptTriggered = true;
}
}
return this;
}

首先判断interruptTriggered,如果为True,立即返回;如果为False,调用setWakeupSocket(),并将interruptTriggered设置为true。下面看setWakeupSocket()的实现:

private void setWakeupSocket() {
this.setWakeupSocket0(this.wakeupSinkFd);
}
private native void setWakeupSocket0(int wakeupSinkFd);

传入管道sink端的wakeupSinkFd,然后调用底层的setWakeupSocket0方法,下面从openjdk8源文件WindowsSelectorImpl.c找到setWakeupSocket0的实现:

Java_sun_nio_ch_WindowsSelectorImpl_setWakeupSocket0(JNIEnv *env, jclass this,
jint scoutFd)
{
/* Write one byte into the pipe */
const char byte = 1;
send(scoutFd, &byte, 1, 0);
}

该函数的主要作用是向pipe的sink端写入了一个字节,这样pipe的source端文件描述符立即就会处于就绪状态,select()方法将立即从阻塞中返回,这样就完成了唤醒selector的功能。

wakeup()中使用interruptTriggered来判断是否执行唤醒操作。因此,在select期间,多次调用wakeup()产生的效果与调用一次是一样的,因为后面的调用将不会满足唤醒条件。如果调用wakeup()期间没有select操作,当调用wakeup()之后,interruptTriggered被设置为true,pipe的source端wakeupSourceFd 就会处于就绪状态。如果此时调用select相关操作时,会调用resetWakeupSocket 方法,resetWakeupSocket 首先会调用本地方法resetWakeupSocket0读取wakeup()中发送的数据,再将interruptTriggered设置为false,最后doSelect将会立即返回0,而不会调用poll操作。

为什么说将一些字节写入到SinkChannel后,SourceChannel就可以立即按写入顺序读取这些字节?

这是因为我们在WindowsSelectorImpl构造方法中将TCP参数TCP_NODELAY设置为了true。该参数的主要作用是禁用Nagle算法,当sink端写入1字节数据时,将立即发送,而不必等到将较小的包组合成较大的包再发送,这样source端就可以立马读取数据。

下面附上windows环境下Selector的实现原理图帮助理解阻塞与唤醒的原理(图片来源网络):

总结

本文主要介绍了windows环境下wakeup()的实现原理,它通过一个可写的SinkChannel和一个可读的SourceChannel组成的pipe来实现唤醒的功能,而Linux环境则使用其本身的Pipe来实现唤醒功能。无论windows还是linux,wakeup的思想是完全一致的,只不过windows没有Pipe这种信号通知的机制,所以通过TCP来实现了Pipe,建立了一对自己和自己的loopback的TCP连接来发送信号。请注意,每创建一个Selector对象,都会创建一个Pipe实例,这会导致消耗两个文件描述符FD和两个端口号,实际开发中需注意端口号和文件描述符的限制。

Java NIO wakeup实现原理的更多相关文章

  1. Java NIO使用及原理分析 (四)

    在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...

  2. Java NIO使用及原理分析 (四)(转)

    在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ...

  3. Java NIO使用及原理分析(1-4)(转)

    转载的原文章也找不到!从以下博客中找到http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一 ...

  4. Java NIO使用及原理分析(二)

    在第一篇中,我们介绍了NIO中的两个核心对象:缓冲区和通道,在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,如 ...

  5. Java NIO使用及原理分析(二)(转)

    在第一篇中,我们介绍了NIO中的两个核心对象:缓冲区和通道,在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,如 ...

  6. Java NIO使用及原理分析 (一)(转)

    最近由于工作关系要做一些Java方面的开发,其中最重要的一块就是Java NIO(New I/O),尽管很早以前了解过一些,但并没有认真去看过它的实现原理,也没有机会在工作中使用,这次也好重新研究一下 ...

  7. Java NIO使用及原理分析(三)

    在上一篇文章中介绍了缓冲区内部对于状态变化的跟踪机制,而对于NIO中缓冲区来说,还有很多的内容值的学习,如缓冲区的分片与数据共享,只读缓冲区等.在本文中我们来看一下缓冲区一些更细节的内容. 缓冲区的分 ...

  8. Java NIO使用及原理分析(三)(转)

    在上一篇文章中介绍了缓冲区内部对于状态变化的跟踪机制,而对于NIO中缓冲区来说,还有很多的内容值的学习,如缓冲区的分片与数据共享,只读缓冲区等.在本文中我们来看一下缓冲区一些更细节的内容. 缓冲区的分 ...

  9. Java NIO使用及原理分析 (一)

    http://blog.csdn.net/wuxianglong/article/details/6604817

随机推荐

  1. Java中的异常处理机制《》

    异常机制已经成为判断一门编程语言是否成熟的标准,异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性. Java异常机制主要依赖于try.catch.finall ...

  2. Java项目开发流程()

    1项目启动 2需求调研 3系统设计详细设计 4程序开发 5测试 6试用培训维护 项目成员组成 1需求工程师其要求 2系统分析师设计师其要求 3开发工程师其要求 4测试工程师其要求 5管理人员 6其他人 ...

  3. Python3 注释、运算符、数字、字符串

    文章目录 注释 单引号(''') 双引号(""") 运算符 数字(Number) Python 数字类型转换 数学函数 随机数函数 三角函数 数学常量 数字与字符,列表之 ...

  4. Docker及其使用思维导图

    学习Docker的使用. 包括Docker的一些命令,Docker容器的使用,Docker镜像的使用,Docker容器连接等内容. 各种思维导图下载地址 Docker使用思维导图及各各种命令 Dock ...

  5. 正则表达式获取字符串的input标签的属性值

    直接上正则表达式: (?<=\<input.*value=\").*?(?=\") 字符串如下: <form action="https://www.b ...

  6. CAS客户端和服务器配置https证书

    关于如何生成https证书可以看这篇文章: java生成Https证书,及证书导入的步骤和过程 下面整理cas如何整合https: cas服务器端部署(TLS[https]) 1.生成证书: 参照ja ...

  7. (31)grep命令详解:查找文件内容

    1.grep命令用于不需要列出文件的全部内容,而是从文件中找到包含指定信息的那些行. grep命令能够在一个或多个文件中,搜索某一特定的字符模式(也就是正则表达式),此模式可以是单一的字符.字符串.单 ...

  8. Python开发桌面微型计算器

    开发Windows窗口需要用到tkinter库 所以上来的第一件事就是: import tkinter as t window = t.Tk()#创建了一个窗口 window.title('微型计算器 ...

  9. 深度学习论文翻译解析(十九):Searching for MobileNetV3

    论文标题:Searching for MobileNetV3 论文作者:Andrew Howard, Mark Sandler, Grace Chu, Liang-Chieh Chen, Bo Che ...

  10. inceptor es表插入成功,返回报错you should set transaction.type before any DCL statement

    在finebi下用星环的连接驱动去写inceptor es表,发现插入能成功,但是返回一个报错: Caused by: java.sql.SQLException: Error to commit. ...