Java NIO选择器select()方法返回0,尽管通道已准备就绪。

12
我的Java NIO选择器是使用select()实现的,因此它会阻塞直到发生以下情况之一:
  1. 已注册的通道准备就绪
  2. 它被wakeup()唤醒
  3. 线程被中断
基于此,我对select()返回0的情况做出了一些假设:
  • 必须是第2或第3种情况。
  • selectedKeys()应返回一个空的ResultSet
  • 我不需要调用selectedKeys(),可以继续下一个循环迭代,在其中将调用select()
然而,我遇到了这样的情况,即使有一个就绪的通道,select()返回了0。如预期,selectedKeys()返回一个包含1个SelectionKeySet
即使多次调用select(),直到处理通道并移除SelectionKey之前,它仍然总是返回0。这种情况基本上会陷入无限循环,因为select()不会阻塞,而是总是立即返回0。
简化代码:
Selector selector = Selector.open();

SocketChannel channel;

for (...) { // for each node
  // Create and connect channels...
  ...

  channel.configureBlocking(false);
  channel.register(selector, SelectionKey.OP_READ, someRelatedObject);
}

int ready;
Set<SelectionKey> readyKeys;
while (true) {
  ready = selector.select();
  readyKeys = selector.selectedKeys();

  System.out.println("Ready channels: " + ready);
  System.out.println("Selected channels: " + readyKeys.size());

  if (ready == 0) {
    continue;
  }

  for (SelectionKey key : readyKeys) {
    if (key.isValid() && key.isReadable()) {
      // Take action...
    }

    readyKeys.remove(key);
  }
}

为什么select()会返回0,即使有一个准备好的通道?应该如何处理?
编辑:
将此更改为:
  for (SelectionKey key : readyKeys) {
    if (key.isValid() && key.isReadable()) {
      // Take action...
    }

    readyKeys.remove(key);
  }

到这里

  for (SelectionKey key : readyKeys) {
    readyKeys.remove(key);

    if (key.isValid() && key.isReadable()) {
      // Take action...
    }
  }

解决了问题。在某些情况下,代码会在remove()删除键之前继续for循环。 编辑2: 我最近才学到我的选定键集上的foreach循环是错误的。foreach使用集合的迭代器。在迭代过程中直接修改集合(而不是通过迭代器的方法)可能导致“任意,不确定”的行为。
选定的键集可以提供一个fail-fast迭代器。 fail-fast迭代器检测到这种修改并在下一次迭代时抛出ConcurrentModificationException。因此,在foreach中修改集合或者冒着不确定的行为或者可能引起异常-具体取决于迭代器的实现。
解决方案:不要使用foreach。使用迭代器,并通过iterator.remove()删除键。
Iterator<SelectionKey> iterator;
SelectionKey key;
while (true) {
  // ...

  iterator = selector.selectedKeys().iterator();
  while (iterator.hasNext()) {
    key = iterator.next();
    iterator.remove();
    // ...
  }
}
2个回答

10

select() 返回已经发生改变的键的数量。如果在 select() 调用之前某个键已经准备好了,那么它可能会返回 0,但是 selectedKeys 可能不为空。


3
正如你所指出的,这是因为你没有从选定的集合中删除所选的键。
通常我们希望在处理完集合后移除所有选定的键,你可以在循环结束后调用clear()方法来清空该集合,它可以是foreach或任何类型的循环。
Set<SelectionKey> readyKeys = selector.selectedKeys();
for (SelectionKey k : readyKeys) {
    // Process k
}
readyKeys.clear();

这样一来,您可以使用任何类型的循环("普通" for 或迭代器),并确保所有准备好的键都被移除,无论您在for内部执行什么操作(包括有问题的continue)。根据iterator.remove()在内部执行的操作,调用clear()一次可能比多次调用iterator.remove()更有效率(尽管这可能是微小的优化)。


1
不回答问题:“为什么select()返回0,尽管有准备好的通道?” - user207421
@EJP您是对的,我已经纠正了它。这样就足以取消您的踩了吗? - Matthieu
通过 iterator.remove() 立即删除每个 SelectionKey 的优势是什么? - riha
2
@riha:选择你认为更合适的循环类型,并确保不要忘记任何关键部分。除非在某些情况下,你想要对何时/是否删除一个键有更精细的控制。我已经编辑了我的答案,包括这一点。 - Matthieu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接