Better SelectableChannel registration in Java NIO
Java NIO’s Selector class is surprisingly difficult to use with multiple threads. Everyone that tries it encounters mysterious blocking, much of which is due to it sharing a lock with SelectableChannel.register
. So, if you happen to try to register a channel in a thread other than the selector thread, it blocks that thread until the select
is done. Boo.
So, this is a NIO rite of passage of sorts, finding this misfeature and then looking up how to work around it. The usual answer is to keep a ConcurrentQueue of pending registrations, and have your select loop process that queue between select
calls. Uggggleeee. It occurred to me that using a synchronization lock, we can do better.
To register a channel and get the SelectionKey:
synchronized(registerLock) {
selector.wakeup();
key = channel.register(selector, operations, attachment);
}
And in the select loop:
// before
synchronized(registerLock) {}
// between
numEvents = selector.select(timeout);
// after
If the loop is before
or after
when our registration block takes the registerLock
, we’re fine, as having the registerLock
prevents the loop from reaching select
until we’ve registered and released the lock. If the loop is inside select()
, then the wakeup()
will cause it to exit select
, and it won’t re-enter because we hold the registerLock
, so we’re fine.
The tricky case is when the loop has the registerLock
or is between
releasing the registerLock
and entering select()
. In these cases, the registration block takes the registerLock
and races with the loop over select()
and wakeup()
. Fortunately, the NIO designers anticipated that programmers would have a desire to ensure a Selector wasn’t selecting, even if the wakeup
was called in the window between checking it was okay to enter select
and actually entering it. Selector.select()
returns immediately if wakeup()
had been called after that Selector’s prior select()
. So, our race doesn’t matter, the select()
always exits, and we’re safe.
This is so much simpler than building up a queue of registrations and processing them in the select loop, and we get the SelectionKey right away, I wonder if I’m missing something. Why is the textbook technique to use a ConcurrentQueue, instead of a synchronization lock like this?