Java线程在调用select()方法时注册通道时被阻塞,怎么办?

18

我有一个基础问题。为什么和怎样可选通道(SelectableChannel)的register方法可以是阻塞调用呢?让我提供一个场景。

我在Register类中创建了一个选择器(Selector)对象,如下所示:

private static Selector selector = Selector.open();

我还有一个在同一类(Register)中的方法注册通道到选择器(selector)。

public static SelectionKey registerChannel(SelectableChannel channel, int ops)
                             throws IOException {
   channel.configureBlocking(false);
   return channel.register(selector, ops);
}

还有另一个名为Request的类,它有一种方法可以从通道中读取数据,处理并调用以下方法来注册该通道。

selectonKey = Register.register(socketChannel, SelectionKey.OP_READ);

在这个地方,线程被阻塞了,没有任何提示它在等待什么。我已经确认选择器是开放的。请提供帮助让我了解如何解决这个问题。是否有任何锁可以释放。

任何建议都将不胜感激。

补充我所描述的情况。进一步测试揭示,如果从同一个线程调用Register.register方法,它能够注册,但之后如果其他线程尝试调用该方法,线程就无法继续执行。

5个回答

40

这是大多数NIO实现的一个基本特性,文档中并不明显。

你需要从执行选择操作的同一线程中进行所有注册调用,否则将会发生死锁。通常,这是通过提供一个注册/取消注册/兴趣更改队列来完成的,该队列被写入后调用selector.wakeup()。当选择线程醒来时,它会检查队列并执行任何请求的操作。


7
这是所有 NIO 实现的基本特征,其在 Javadoc 中完全规定。当您处于 select() 中时,有三个嵌套的同步操作,并且 register() 尝试其中之一。结果不是死锁而是阻塞,只持续到并发的 select() 调用返回为止。解决方法是在 register() 之前调用 wakeup()。这会强制 select() 解除阻塞并返回零,释放其三个锁,允许 register() 获取所需的一个并继续进行。 - user207421
3
上面评论中提供的解决方案是错误的——至少根据解释来看是这样。根据线程的调度,选择线程可能会完成整个循环并在注册线程能够调用register()之前重新进入select()。 - dsd
当前Java 8 javadoc仅在SelectableChannel.register()中提到了此问题。AbstractSelectableChannel.register省略了任何提及(并省略了一些其他细节)。我有这样的代码:SocketChannel sc = SocketChannel.open(); . . . sc.register(....);最后一行调用AbstractSelectableChannel.register()。因此,如果在IDE中查看Javadocs,则看不到SelectableChannel.register()的文档。 - jimvfr
1
如果 select() 返回零,则短暂的休眠就可以解决这个问题。 - user207421

14
你需要使用锁并手动进行同步。
在你运行选择器循环的同一线程中,有一个可重入锁:
final ReentrantLock selectorLock = new ReentrantLock();

然后当你需要用选择器进行注册时,可以像这样做:

selectorLock.lock();
try {
    selector.wakeup();
    socketChannel.register(selector, ops);
} finally {
    selectorLock.unlock();
}

最后,在调用accept()的循环期间,做如下操作:

selectorLock.lock();
selectorLock.unlock();

selector.select(500);

然后继续执行余下的逻辑。

这个结构确保register()调用不会被阻塞,通过确保在相应的wakeup()register() 调用之间永远不存在其他select()调用。


2
+1 对于代码示例。从我的经验来看,我也同意这一点。做得很好。 - casey
2
这会给每个注册增加500毫秒的不必要的延迟。请使用https://dev59.com/pHNA5IYBdhLWcg3wL6yx#2179612中的方法。 - David B.
@DavidB。它不会增加任何注册的延迟。阻塞的是选择线程,而不是注册线程,而且选择线程始终希望在select()中阻塞。你所提供答案中的大部分声明都是不正确的。 - user207421
我在实现时遇到了一个问题。一开始,在选择器循环中,除了selector.select(500)语句之外,没有其他任务在selectorLock.lock()selectorLock.unlock()之间,因为还没有注册通道。循环速度非常快,另一个线程很难介入并获取selectorLock(需要30秒到几分钟)。如果我在while循环中但在selectorLock.lock()selectorLock.unlock()之外向控制台打印一些内容,其他线程就有机会获取选择器锁。如果循环中没有任务,则可能需要睡眠几毫秒。 - zipper
1
异议:仍存在竞态条件。假设“选择”线程通过锁定/解锁行,然后暂停。另一个线程开始“注册”块,完成“唤醒”行,然后暂停。选择线程恢复,并开始选择调用。这是对结论性语句的反例。我对在此处引起死锁的确切条件有些模糊,但某种形式的“选择”和“注册”同时发生,可以从给定情况中达到,应该可以解决问题。超时可能会防止完全死锁,但仍会浪费时间。 - Erhannis

3
我同意@Darron的答案,你应该将register调用传递给选择器线程,但不应使用selector.wakeup,因为这会引入竞争条件(想象一下选择器线程正在忙于处理其他注册,而你的wakeup无法唤醒任何人)。幸运的是,Java NIO提供了Pipe,这样你就可以让选择器同时监听register调用和其他事件。
基本上,需要执行以下操作:
val registrationPipe = Pipe.open()
registrationPipe.source().configureBlocking(false)
registrationPipe.source().register(selector, SelectionKey.OP_READ)
// now start your selector thread

// now to register a call from other threads using message pleaseRegisterMe
registrationPipe.sink().write(pleaseRegisterMe)

// inside your selector thread
val selectionKey = iterator.next()
if (selectionKey.channel() === registrationPipe.source()) {
    registrationPipe.source().read(pleaseRegisterMe)
    // do something with the message pleaseRegisterMe and do the actual register
}

这里有一个完整的工作示例


0

可以从任何线程注册您的频道:

synchronized (selectorLock2) {
   selector.wakeup();
   synchronized (selectorLock1) {
       channel.register(selector, ops);
   }
}

你的选择器循环应该长这样:

while (true) {
   synchronized (selectorLock1) {
       selector.select();
   }
   synchronized (selectorLock2) {}

   ....
}

1
你只需要一个单一的锁,如 https://dev59.com/pHNA5IYBdhLWcg3wL6yx#1112809 中所示。 - Flow

0

你尝试过打印程序中所有线程的堆栈跟踪吗(可以使用Unix中的kill -QUIT或Windows中的Ctrl+Break,或使用jstack实用程序)?

AbstractSelectableChannel 包含一个锁,configureBlockingregister 需要同步。这个锁也可以通过 blockingLock() 方法访问,因此另一个线程可能会持有该锁,导致您的注册调用无限期地阻塞(但没有堆栈跟踪很难确定原因)。


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