NIO中的Selector封装了底层的系统调用,其中wakeup用于唤醒阻塞在select方法上的线程,它的实现很简单,在linux上就是创建一 个管道并加入poll的fd集合,wakeup就是往管道里写一个字节,那么阻塞的poll方法有数据可读就立即返回。证明这一点很简单,strace即 可知道:
- public class SelectorTest {
- public static void main(String[] args) throws Exception {
- Selector selector = Selector.open();
- selector.wakeup();
- }
- }
使用strace调用,只关心write的系统调用
- sudo strace -f -e write java SelectorTest
输出:
- Process 29181 attached
- Process 29182 attached
- Process 29183 attached
- Process 29184 attached
- Process 29185 attached
- Process 29186 attached
- Process 29187 attached
- Process 29188 attached
- Process 29189 attached
- Process 29190 attached
- Process 29191 attached
- [pid 29181] write(36, "1", 1) = 1
- Process 29191 detached
- Process 29184 detached
- Process 29181 detached
有的同学说了,怎么证明这个write是wakeup方法调用的,而不是其他方法呢,这个很好证明,我们多调用几次:
- public class SelectorTest {
- public static void main(String[] args) throws Exception {
- Selector selector = Selector.open();
- selector.wakeup();
- selector.selectNow();
- selector.wakeup();
- selector.selectNow();
- selector.wakeup();
- }
- }
修改程序调用三次wakeup,心细的朋友肯定注意到我们还调用了两次selectNow,这是因为在两次成功的select方法之间调用wakeup多 次都只算做一次,为了显示3次write,这里就每次调用前select一下将前一次写入的字节读到,同样执行上面的strace调用,输出:
- Process 29303 attached
- Process 29304 attached
- Process 29305 attached
- Process 29306 attached
- Process 29307 attached
- Process 29308 attached
- Process 29309 attached
- Process 29310 attached
- Process 29311 attached
- Process 29312 attached
- Process 29313 attached
- [pid 29303] write(36, "1", 1) = 1
- [pid 29303] write(36, "1", 1) = 1
- [pid 29303] write(36, "1", 1) = 1
- Process 29313 detached
- Process 29309 detached
- Process 29306 detached
- Process 29303 detached
果然是3次write的系统调用,都是写入一个字节,如果我们去掉selectNow,那么三次wakeup还是等于一次:
- public class SelectorTest {
- public static void main(String[] args) throws Exception {
- Selector selector = Selector.open();
- selector.wakeup();
- selector.wakeup();
- selector.wakeup();
- }
- }
输出:
- Process 29331 attached
- Process 29332 attached
- Process 29333 attached
- Process 29334 attached
- Process 29335 attached
- Process 29336 attached
- Process 29337 attached
- Process 29338 attached
- Process 29339 attached
- Process 29340 attached
- Process 29341 attached
- [pid 29331] write(36, "1", 1) = 1
- Process 29341 detached
- Process 29337 detached
- Process 29334 detached
- Process 29331 detached
wakeup方法的API说明没有欺骗我们。wakeup方法的API还告诉我们,如果当前Selector没有阻塞在select方法上,那么本次 wakeup调用会在下一次select阻塞的时候生效,这个道理很简单,wakeup方法写入一个字节,下次poll等待的时候立即发现可读并返回,因 此不会阻塞。
具体到源码级别,在linux平台上的wakeup方法其实调用了pipe创建了管道,wakeup调用了EPollArrayWrapper的interrupt方法:
- public void interrupt()
- {
- interrupt(outgoingInterruptFD);
- }
实际调用的是interrupt(fd)的native方法,查看EPollArrayWrapper.c可见清晰的write系统调用:
- JNIEXPORT void JNICALL
- Java_sun_nio_ch_EPollArrayWrapper_interrupt(JNIEnv *env, jobject this, jint fd)
- {
- int fakebuf[1];
- fakebuf[0] = 1;
- if (write(fd, fakebuf, 1) < 0) {
- JNU_ThrowIOExceptionWithLastError(env,"write to interrupt fd failed");
- }
- }
写入一个字节的fakebuf。有朋友问起这个问题,写个注记在此。strace充分利用对了解这些细节很有帮助。