当遍历键时,为什么会出现CancelledKeyException?

12

我为什么会每天遇到几次CancelledKeyException?我该做些什么吗?我的代码有问题吗?

        Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
        while (keys.hasNext()) {

            SelectionKey key = (SelectionKey) keys.next();
            keys.remove();

            try {
                if (key.isValid()) {
                    if (key.isReadable()) {
                        readHandler.handle((Connection) key.attachment());
                    }
                    if (key.isWritable()) {
                        writeHandler.handle((Connection) key.attachment());
                    }
                    if (key.isAcceptable()) {
                        acceptHandler.handle(key);
                    }
                }
            } catch (CancelledKeyException e) {
                _logger.error("CanceledKeyException in while loop:", e);
            }
        }

异常:

java.nio.channels.CancelledKeyException: null
    at sun.nio.ch.SelectionKeyImpl.ensureValid(SelectionKeyImpl.java:55) ~[na:1.6.0_12]
    at sun.nio.ch.SelectionKeyImpl.readyOps(SelectionKeyImpl.java:69) ~[na:1.6.0_12]
    at java.nio.channels.SelectionKey.isWritable(SelectionKey.java:294) ~[na:1.6.0_12]
    at project.engine.io.SimpleReactor.work(SimpleReactor.java:194) ~[engine-02.06.11.jar:na]
    at project.server.work.AbstractWorker$1.run(AbstractWorker.java:20) [server-21.05.11.jar:na]
    at java.lang.Thread.run(Thread.java:619) [na:1.6.0_12]

异常状态是什么?请发布堆栈跟踪。 - Buhake Sindi
@the-elite-gentleman,嗯,没有堆栈跟踪,但是当我记录e时只有这个:java.nio.channels.CancelledKeyException。很奇怪。 - Rihards
1
什么?如果抛出异常,它应该始终包含堆栈跟踪。这是由Throwable继承的,所以我不明白为什么这是可能的。 - Voo
@voo,糟糕。我记录错误了。等我获取到它后会发布堆栈跟踪。 - Rihards
@the-elite-gentleman,关于这个异常你有什么想法吗? - Rihards
答案不应该是问题的一部分,所以我回滚到了之前的版本。(如果有错误请纠正我) - user253751
3个回答

8

一个处理程序可能会关闭通道。例如,如果读取处理程序读取到-1,应该关闭通道。因此,写处理程序将失败。确实,从您的堆栈跟踪中我现在可以看到isWritable()也将失败。因此,您必须测试isValid()与每个其他条件,例如 isValid() && isReadable(), isValid() && isWritable()等。


哦,我现在是否应该像我现在所拥有的那样保留 isValid() 在所有的 isReadable() 之前,并且只是像你说的那样将 isValid() 添加到每个 isReadable() 中?或者不需要保留我现在拥有的 isValid() - Rihards
1
@Richards,你现在的东西不够好。否则你就不会在这里发帖了。改进它。 - user207421
@Richards:按照我上面说的改一下。我甚至给了你代码。 - user207421

0
EJP的解决方案似乎不是最理想的...我认为正确处理在选择器循环中被取消的选择键的方法是用try/catch将其包围,并检查CancelledKeyException,如下所示:
try {
   if (key.isReadable())
      processMessage(key);

   if (key.isAcceptable())
      acceptConnection(key);
}
catch (CancelledKeyException e) {
   logger.debug("Key cancelled... Closing channel.");
   key.channel().close();
}

按照哪个标准来说,这不是理想的呢? - user207421
1
因为我认为这并没有解决问题。问题在于密钥在调用key.isValid()之后和isWritable() / isReadable() / isAcceptable()调用之前被取消。我没有看到OP的代码和你建议的有什么区别。唯一的区别是你检查密钥是否有效三次,而OP每次迭代循环只检查一次。你的解决方案可能会起作用,因为你给了这个并发问题更少的时间来显现。但它仍然存在,没有解决。 - Eduardo Bezerra
只有在存在多个线程的情况下才会存在并发问题,但是在原始问题中并没有涉及到多线程,也通常不会在NIO中出现这种情况,因为NIO的设计目的就是消除线程。 - user207421
2
那取决于NIO内部的实现方式。您确定它在任何架构中都没有使用/依赖于多个线程吗?尽管如此,我所谈论的并发性是在本地机器和远程计算机之间的。它们同时执行。例如,远程主机可能在OP调用isValid()之后断开连接,并且相应的键在他尝试访问该键之前就被取消了。这就是为什么在每次访问之前调用key.isValid()有时会起作用。您是否还有其他解释? - Eduardo Bezerra
NIO 中没有内部线程,它只使用您自己使用的线程。远程主机断开连接不会取消密钥,只有本地应用程序才能这样做。远程断开实际上会导致密钥变为可读状态,并且有一个待读取的流结束标记。 - user207421

0
在我的情况下,执行取消操作的线程与注册通道进行选择的线程是不同的。当键首先被取消,然后另一个线程在没有中间的选择操作的情况下再次注册键时,似乎会出现此异常。因此,我通过在再次注册通道之前执行一个虚假的选择操作来解决这个问题。似乎选择操作中进行了一些簿记工作,这对于注册操作成功非常重要。 两个线程都在共享的静态对象上使用同步块(总共3个),以锁定注册、取消和选择操作。

进一步测试后发现,执行选择操作但未处理所选键会导致致命错误。当我说“虚拟选择”时,我指的是一个带有1L超时的选择操作,其所选键与其他线程处理方式相同。 - Heymen Nicolaij

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