Building Highly Scalable Servers with Java NIO
Pages: 1, 2, 3
After being registered, handlers can activate or deactivate interest in specific I/O events
by updating their interest sets. Internally, the SelectorThread
class updates the interest set of the corresponding SelectionKey. There is
no support for de-registering a channel, since this can be easily accomplished
by closing the socket.
Here is what the SelectorThread class looks like:
public class SelectorThread implements Runnable {
/**
* Graceful shutdown.
*/
public void requestClose() {
...
}
/**
* Adds a new interest to the list of events
* where a channel is registered.
*/
public void addChannelInterestNow(
SelectableChannel channel,
int interest) throws IOException {
...
}
/**
* Like addChannelInterestNow(), but executed
* asynchronously on the selector thread.
*/
public void addChannelInterestLater(
SelectableChannel channel,
int interest,
CallbackErrorHandler errorHandler) {
...
}
/**
* Removes an interest from the list of events
* where a channel is registered.
*/
public void removeChannelInterestNow(
SelectableChannel channel,
int interest) throws IOException {
...
}
/**
* Like removeChannelInterestNow(), but executed
* asynchronously on the selector thread.
*/
public void removeChannelInterestLater(
SelectableChannel channel,
int interest,
CallbackErrorHandler errorHandler) {
...
}
/**
* Like registerChannelLater(), but executed
* asynchronously on the selector thread.
*/
public void registerChannelLater(
SelectableChannel channel,
int selectionKeys,
SelectorHandler handlerInfo,
CallbackErrorHandler errorHandler) {
...
}
/**
* Registers a SelectableChannel with this
* selector.
*/
public void registerChannelNow(
SelectableChannel channel,
int selectionKeys,
SelectorHandler handlerInfo)
throws IOException {
...
}
/**
* Executes the given task in the selector
* thread. Does not wait for its execution.
*/
public void invokeLater(Runnable run) {
...
}
/**
* Executes the given task synchronously in the
* selector thread, waiting for its execution.
*/
public void invokeAndWait(final Runnable task)
throws InterruptedException
{
...
}
/**
* Main cycle. This is where event processing
* and dispatching happens.
*/
public void run() {
...
}
}
The purpose of the invoke*() methods and
of the two variants (*Now() and
*Later()) for most of the public methods will be explained shortly.
Threading in a Multiplexed World
In theory, with I/O multiplexing it is possible to have a
single thread do all of the work in a server application. In practice, that is a
very bad idea. When using a single thread, it is not possible to hide the
latency of disk I/O (Java NIO does not support non-blocking file operations) or
to take advantage of systems with multiple CPUs. As a rough guideline, a server
application should have at least 2*n threads, with n being the number of
execution units available. Therefore, we had to implement a way of dividing the
work among threads.
Getting the threading model right was the hardest part of the development. We considered the following architectures:
Mdispatchers/no workers
Several event-dispatch threads doing all the work (dispatching events, reading, processing requests, and writing).1 dispatcher/
Nworkers
A single event-dispatch thread and multiple worker threads.Mdispatchers/Nworkers
Multiple event dispatch threads and multiple worker threads.
In all cases, incoming connections are assigned to an
event-dispatch thread (a SelectorThread) for the duration of
their lives. In the first architecture, I/O events are fully processed by
the event-dispatch thread. In the other two, the processing is delegated
to worker threads.