Top Ten New Things You Can Do with NIO
Pages: 1, 2, 3, 4
2: Non-Blocking Sockets
The lack of non-blocking I/O in the traditional Java I/O model has
been conspicuous from the start. It's
finally arrived with NIO. Channel classes that extend from
SelectableChannel can be placed into non-blocking mode
with the configureBlocking() method. As of the J2SE 1.4
release, only the socket channels (SocketChannel,
ServerSocketChannel, and DatagramChannel) may
be placed into non-blocking mode. FileChannel cannot be
placed in non-blocking mode.
When a channel is non-blocking, read() or
write() calls always return immediately, whether they
transferred any data or not. This enables a thread to check if data is
available without getting stuck.
ByteBuffer buffer = ByteBuffer.allocate (1024);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking (false);
...
while (true) {
...
if (socketChannel.read (buffer) != 0) {
processInput (buffer);
}
...
}
The code above represents a typical polling loop. A
non-blocking read is attempted, and if some data was read, it's
processed. A return value of zero from the read() call
indicates no data is available, and the thread trundles on through the
body of the main loop, doing whatever else it does on each pass.
1: Multiplexed I/O
And now ladies and germs, the Number One New Thing You Can Do With NIO That You Couldn't Do Before.
The code example in the previous section uses polling to determine when input is ready on a non-blocking channel. There are situations where this is appropriate, but usually, polling is not very efficient. In a case where your processing loop is primarily doing something else and periodically checking for input, polling might be an appropriate choice.
But if the application's primary purpose is to respond to input arriving on many different connections (a Web server, for example), polling doesn't work so well. To be responsive, you need to poll quickly. But polling quickly needlessly burns tons of CPU cycles and generates massive numbers of unproductive I/O requests. I/O requests generate system calls, system calls entail context switches, and context switches are expensive.
When a single thread is managing many I/O channels, this is known as multiplexed I/O. For multiplexing, what you really want to do is have the managing thread block until input is available on at least one of the channels. But hey, weren't we just doing the happy dance about finally having non-blocking I/O? We had blocking I/O before, what the...?
The problem with the conventional blocking model is that a single thread can't multiplex a group of I/O streams. Without non-blocking mode, a read attempt by the thread on a socket with no data available would block the thread, thereby preventing it from taking care of other streams that may have data to read. The net effect is that one idle stream would suspend servicing of all streams.
The solution to this problem in the Java world has historically
been to dedicate a thread to each active stream. As data became
available on a given stream, its dedicated thread would wake up and
read the data, process it, then block in a read() again
until more data showed up. This process actually works, but it is not
scalable. Threads (which are rather heavyweight to create) multiply at
the same rate as sockets (which are relatively lightweight). The
thread creation overhead can be mitigated somewhat by pooling and
reusing them (more complexity and code that needs to be debugged), but
the main problem is that it stresses the thread scheduler as the
number of threads grows larger. The JVM thread-management machinery is
designed to handle a few tens of threads, not hundreds or thousands.
Even idle threads can slow things down considerably. Thread-per-stream
may also introduce nasty concurrency issues if the stream-draining
threads must funnel their data to common data-handling objects.
The right way to multiplex large numbers of sockets is with readiness
selection (which takes the form of the Selector class
in NIO). Selection is a big win over polling or thread-per-stream,
because a single thread can monitor a large number of sockets
easily. A thread can also (and here's where we get back to
blocking again) choose to block and be awakened when any of those
streams have data available (the readiness part) and receive
information about exactly which streams are ready to go (the selection
part).
Readiness selection is built on top of non-blocking mode; it only
works with channels that have been placed in non-blocking mode. The
actual selection process can also be non-blocking, if you prefer ("find
out what's ready right now"). The key point is that a Selector
object does the hard work of checking the state of a (potentially
large) number of channels. You just act on the result of selection;
you don't need to check each one yourself.
You create a Selector instance, then register one or
more non-blocking channels with it, indicating for each what events
are of interest. Below is a prototypical selection loop. In this
example, incoming connections on a ServerSocketChannel
object are serviced in the same loop as active socket connections
(more complete examples are available in my book):
ServerSocketChannel serverChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
serverChannel.socket().bind (new InetSocketAddress (port));
serverChannel.configureBlocking (false);
serverChannel.register (selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Iterator it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
channel.configureBlocking (false);
channel.register (selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
readDataFromSocket (key);
}
it.remove();
}
}
This is much simpler and far more scalable than the thread-per-socket arrangement. It's much easier to write and debug code like this, but just as importantly, the effort required to manage and service large numbers of sockets is vastly reduced. Selectors, probably more than any other new feature in NIO, delegate grunt work to the OS. That relieves the JVM of a huge amount of work, which frees memory and CPU resources and allows tremendous scaling because the JVM isn't spending time doing work the OS can do much more easily.
Summary
Well, there you have it. Ten things you can do with NIO in J2SE 1.4 that you couldn't do before in Java. This is by no means a complete list. There are plenty of other things I didn't mention, like custom character set transcoding, pipes, asynchronous socket connection, copy-on-write mapped files, and so on. I'd need an entire book to cover everything. ;-)
I hope these brief glimpses have given you an idea of what NIO can do, as well as what you can do with NIO in your own projects. NIO is not a replacement for the traditional I/O classes; don't throw away your working code just to replace it with NIO. But keep NIO in mind as you design new applications. You just might find that NIO can kick some butt when you turn it loose.
O'Reilly & Associates recently released (August 2002) Java NIO.
Sample Chapter 4, "Selectors," is available free online.
You can also look at the Table of Contents, the Index, and the full description of the book.
For more information, or to order the book, click here.
Ron Hitchens is a California-based computer consultant and educator whose career dates back to the disco era. Ron has used just about every computer system and programming language you can imagine: from 6502 assembler to XSLT. He is also the author of O'Reilly's Java NIO.
Return to ONJava.com.