【转】Selector.wakeup实现注记

本文探讨了NIO中Selector的wakeup方法如何通过写入一个字节到管道来唤醒阻塞在select方法上的线程。并通过strace工具验证了这一机制的有效性。

版权信息:

原文地址:http://www.blogjava.net/killme2008/archive/2010/10/22/335861.html

原文作者:dennis.zane

 

 

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  - - 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调用了EPollArrayWrapperinterrupt方法:

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充分利用对了解这些细节很有帮助。

<think> 题目要求找出描述错误的选项,涉及Java NIO的ServerSocketChannel和Selector的使用。下面逐行分析代码: 1. `ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();`:正确打开ServerSocketChannel。 2. `serverSocketChannel.configureBlocking(false);`:设置为非阻塞模式(正确,注册Selector必须是非阻塞)。 3. `serverSocketChannel.bind(new InetSocketAddress(7880));`:绑定端口7880(正确)。 4. `Selector selector = Selector.open();`:打开Selector(正确)。 5. `serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);`:向Selector注册ACCEPT事件(正确)。 6. `while (true) {`:无限循环(正确)。 7. `int vari = selector.select();`:等待就绪事件,返回就绪事件数量(正确)。 8. `Iterator<SelectionKey> it = selector.selectedKeys().iterator();`:获取就绪事件集合的迭代器(正确)。 9. `while (it.hasNext()) {`:遍历就绪事件(正确)。 10. `SelectionKey selectionKey = it.next();`:获取下一个事件(正确)。 11. 省略处理逻辑(正确)。 12. `it.remove();`:移除当前事件(正确,避免重复处理)。 13. 省略大括号。 选项分析: A. `selector.select()`会阻塞,直到有事件就绪。其他线程可通过`selector.wakeup()`唤醒。正确,select()方法会阻塞,wakeup()可以唤醒它。 B. `int vari = selector.select();` 返回就绪事件个数。正确,select()方法返回的是就绪的通道数。 C. ServerSocketChannel注册Selector前必须设置为非阻塞模式。正确,代码中第2行已经设置。 D. `it.remove();`会引发运行异常。错误,这里不会引发异常。在迭代过程中调用迭代器的remove()方法是安全的,用于移除已经处理的事件,避免重复处理。 因此,描述错误的是D。 答案:D </think> 根据提供的 Java NIO 代码段,我们将逐一分析各选项的正确性: ```java ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 1 serverSocketChannel.configureBlocking(false); // 2 serverSocketChannel.bind(new InetSocketAddress(7880)); // 3 Selector selector = Selector.open(); // 4 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 5 while (true) { // 6 int vari = selector.select(); // 7 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); // 8 while (it.hasNext()) { // 9 SelectionKey selectionKey = it.next(); // 10 // 省略处理事件逻辑 // 11 it.remove(); // 12 } // 13 } // 14 ``` ### 选项分析: **A. `selector.select()`会阻塞,直到有Socket连接到来。其它线程可以通过 `selector.wakeup()` 将 select 从阻塞中唤醒** ✅ **正确** - `selector.select()` 默认会阻塞线程,直到有注册的事件(如新连接)就绪 - `selector.wakeup()` 是唤醒阻塞中 select 的标准方法 **B. `int vari = selector.select();` select 方法返回的是当前就绪事件的个数** ✅ **正确** - `select()` 返回 int 类型值,表示本次触发的事件数量 - `vari` 变量存储的就是就绪事件的数量 **C. ServerSocketChannel 向 Selector 注册,必须通过 `serverSocketChannel.configureBlocking(false)` 设置成非阻塞模式** ✅ **正确** - Java NIO 强制要求:只有非阻塞通道才能注册到 Selector - 第 2 行显示已正确设置非阻塞模式 **D. `it.remove();` 这行代码会引发运行异常** ❌ **错误(题目要求的错误选项)** - `it.remove()` 是**必需且安全**的操作: 1. 从 `selectedKeys` 集合中移除已处理的事件 2. 防止下次循环重复处理同一事件 3. 不会引发异常(正确的集合操作) ### 错误原因说明: 选项 D 描述完全错误,因为: 1. `selectedKeys()` 返回的是 **可修改集合** (HashSet) 2. `remove()` 是迭代器的标准方法 3. 不调用 `remove()` 才会导致事件重复触发 4. 实际运行中不会抛出任何异常 ### 正确结论: **描述错误的是 D 选项** --- ### NIO 事件处理流程解析: ```java while (true) { // 阻塞等待事件发生(可被wakeup打断) int readyCount = selector.select(); // 获取就绪事件集合的迭代器 Iterator<SelectionKey> iter = selector.selectedKeys().iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); // 获取事件 if (key.isAcceptable()) { // 处理新连接请求 SocketChannel client = serverSocketChannel.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_READ); } if (key.isReadable()) { // 处理数据读取 SocketChannel channel = (SocketChannel)key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer); } iter.remove(); // 关键:必须移除已处理事件⚠️ } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值