ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

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.

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.