Java NIO的wakeup剖析

开发 后端
java NIO的实现中,有不少细节点非常有学习意义的,本文主要讲解从Selector的 wakeup原理是什么?是如何实现的?

java NIO的实现中,有不少细节点非常有学习意义的,就好比下面的这个点:

Selector的 wakeup原理是什么?是如何实现的?

wakeup()

准确来说,应该是Selector的wakeup(),即Selector的唤醒,为什么要有这个唤醒操作呢?那还得从Selector的选择方式 来说明,前文已经总结过Selector的选择方式有三种:select()、select(timeout)、selectNow()。
selectNow的选择过程是非阻塞的,与wakeup没有太大关系。

select(timeout)和select()的选择过程是阻塞的,其他线程如果想终止这个过程,就可以调用wakeup来唤醒。

wakeup的原理

既然Selector阻塞式选择因为找到感兴趣事件ready才会返回(排除超时、中断),就给它构造一个感兴趣事件ready的场景即可。下图可以比较形象的形容wakeup原理:

Selector管辖的FD(文件描述符,linux即为fd,对应一个文件,windows下对应一个句柄;每个可选择Channel在创建的时 候,就生成了与其对应的FD,Channel与FD的联系见另一篇)中包含某一个FD A, A对数据可读事件感兴趣,当往图中漏斗端放入(写入)数据,数据会流进A,于是A有感兴趣事件ready,最终,select得到结果而返回。

wakeup在Selector中的定义如下:

  1. public abstract Selector wakeup(); 

下面结合上图来追寻wakeup的实现:

linux下Selector默认实现为PollSelectorImpl,当内核版本大于2.6时,实现为EPollSelectorImpl,仅看这两者的wakeup方法,代码似乎完全一样:

  1. public Selector wakeup() { 
  2.     synchronized (interruptLock) { 
  3.         if (!interruptTriggered) { 
  4.             pollWrapper.interrupt(); 
  5.             interruptTriggered = true
  6.         } 
  7.     } 
  8.     return this

window下Selector的实现为WindowsSelectorImpl,其wakeup实现如下:

  1. public Selector wakeup() { 
  2.     synchronized (interruptLock) { 
  3.         if (!interruptTriggered) { 
  4.             setWakeupSocket(); 
  5.             interruptTriggered = true
  6.         } 
  7.     } 
  8.     return this

其中interruptTriggered为中断已触发标志,当pollWrapper.interrupt()之后,该标志即为true了;得益于这个标志,连续两次wakeup,只会有一次效果。

对比上图及上述代码,其实pollWrapper.interrupt()及setWakeupSocket()就是图中的往漏斗中倒水的过程,不 管windows也好,linux也好,它们wakeup的思想是完全一致的,不同的地方就在于实现的细节了,例如上图中漏斗与通道的链接部 分,linux下是采用管道pipe来实现的,而windows下是采用两个socket之间的通讯来实现的,它们都有这样的特性:

1)都有两个端,一个 是read端,一个是write端,windows中两个socket也是一个扮演read的角色,一个扮演write的角色;

2)当往write端写入 数据,则read端即可以收到数据;从它们的特性可以看出,它们是能够胜任这份工作的。

如果只想理解wakeup的原理,看到这里应该差不多了,不过,下面,想继续深入一下,满足更多人的好奇心。

先看看linux下PollSelector的具体wakeup实现,分阶段来介绍:

1) 准备阶段

PollSelector在构造的时候,就将管道pipe,及wakeup专用FD给准备好,可以看一下它的实现:

  1. PollSelectorImpl(SelectorProvider sp) { 
  2.     super(sp, 1, 1); 
  3.     int[] fdes = new int[2]; 
  4.     IOUtil.initPipe(fdes, false); 
  5.     fd0 = fdes[0]; 
  6.     fd1 = fdes[1]; 
  7.     pollWrapper = new PollArrayWrapper(INIT_CAP); 
  8.     pollWrapper.initInterrupt(fd0, fd1); 
  9.     channelArray = new SelectionKeyImpl[INIT_CAP]; 

IOUtil.initPipe,采用系统调用pipe(int fd[2])来创建管道,fd[0]即为ready端,fd[1]即为write端。

另一个需要关注的点就是pollWrapper.initInterrupt(fd0, fd1),先看一下它的实现:

  1. void initInterrupt(int fd0, int fd1) { 
  2.     interruptFD = fd1; 
  3.     putDescriptor(0, fd0); 
  4.     putEventOps(0, POLLIN); 
  5.     putReventOps(00); 

以看到,initInterrupt在准备wakeup专用FD,因为fd0是read端fd,fd1是write端fd:

interruptFD被初始化为write端fd;

putDescriptor(0, fd0)初始化pollfd数组中的***个pollfd,即指PollSelector关注的***个fd,即为fd0;

putEventOps(0, POLLIN)初始化fd0对应pollfd中的events为POLLIN,即指fd0对可读事件感兴趣;

putReventOps(0, 0)只是初始化一下fd0对应的pollfd中的revents;

2) 执行阶段

有了前面的准备工作,就看PollArrayWrapper中的interrupt()实现:

  1. public void interrupt() { 
  2.     interrupt(interruptFD); 

interrupt是native方法,它的入参interruptFD即为准备阶段管道的write端fd,对应于上图,其实就是漏斗端,因此,就是不看其实现,也知道它肯定扮演着倒水的这个动作,看其实现:

  1. JNIEXPORT void JNICALL 
  2. Java_sun_nio_ch_PollArrayWrapper_interrupt(JNIEnv *env, jobject this, jint fd) 
  3.     int fakebuf[1]; 
  4.     fakebuf[0] = 1; 
  5.     if (write(fd, fakebuf, 1) < 0) { 
  6.          JNU_ThrowIOExceptionWithLastError(env, 
  7.                                           "Write to interrupt fd failed"); 
  8.     } 

可以看出,interrupt(interruptFD)是往管道的write端fd1中写入一个字节(write(fd, fakebuf, 1))。

是的,只需要往fd1中写入一个字节,fd0即满足了可读事件ready,则Selector自然会因为有事件ready而中止阻塞返回。

EPollSelector与PollSelector相比,其wakeup实现就只有initInterrupt不同,它的实现如下:

  1. void initInterrupt(int fd0, int fd1) { 
  2.     outgoingInterruptFD = fd1; 
  3.     incomingInterruptFD = fd0; 
  4.     epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN); 

epfd之前的篇章里已经讲过,它是通过epoll_create创建出来的epoll文件fd,epollCtl调用内核epoll_ctl实现了往epfd上添加fd0,且其感兴趣事件为可读(EPOLLIN)。

因此可以断定,EPollSelector与PollSelector的wakeup实现是一致的。

因为之前一直专注与分析linux下的Java NIO实现,忽略了windows下的选择过程等,这里突然讲解其wakeup实现似乎很突兀,所以打算后面专门起一篇来介绍windows下的NIO实 现,这里我们只需要理解wakeup原理,甚至自己去看看其wakeup实现,应该也没什么难度。

关于wakeup,这里还有两个疑问:

为什么wakeup方法返回Selector?

windows下也是有pipe的,为什么使用socket而不是使用pipe来实现wakeup的?

也欢迎大家留下自己的想法,一起讨论。

原文链接:http://goldendoc.iteye.com/blog/1152079

【编辑推荐】

  1. Java NIO类库关系图解
  2. 浅析Tomcat NIO 配置
  3. Java NIO API详解
  4. Java NIO基本使用实例
  5. Java NIO的介绍及工作原理
责任编辑:林师授 来源: goldendoc的博客
相关推荐

2011-12-08 13:23:00

JavaNIO

2011-12-15 09:40:06

Javanio

2011-12-15 11:19:08

JavaNIO

2011-12-07 14:57:44

JavaNIO

2011-12-15 09:55:47

javanio

2011-12-13 09:12:34

JavaNIO

2011-12-07 14:25:33

JavaNIO

2011-12-15 10:43:20

JavaNIO

2022-02-22 08:00:48

JavaNIOBuffer

2011-12-15 10:10:33

Javanio

2011-12-07 14:41:51

JavaNIO

2015-09-25 09:14:50

java缓冲技术

2011-12-15 12:32:19

JavaNIO

2011-12-08 10:51:25

JavaNIO

2022-01-12 07:36:01

Java数据ByteBuffer

2011-12-02 13:16:14

JavaNIO

2011-12-13 17:31:07

2011-12-15 11:11:51

JavaNIO

2011-12-07 15:58:25

JavaNIO

2011-12-14 10:31:43

点赞
收藏

51CTO技术栈公众号