|
Top Ten New Things You Can Do with NIOby Ron Hitchens, author of Java NIO10/02/2002 |
New I/O? Why do we need a new I/O? What's wrong with the old I/O?
There's nothing wrong with the classes in the java.io
package; they work just dandy -- for what they do. But it turns out
there are quite a lot of things the traditional Java I/O model
can't handle. Things like non-blocking modes, file locks,
readiness selection, scatter/gather, and so on. These capabilities are
widely available on most serious operating systems today (and a few
comical ones, as well). They're not just nice to have; they're
essential for building high-volume, scalable, robust applications,
especially in the enterprise arena.
NIO brings a host of powerful new capabilities to the Java platform. Despite the fact that "N" stands for "New," NIO is not a replacement for the older I/O classes. It's an alternate approach to modeling I/O services, with less emphasis on the streaming model. NIO concentrates on providing consistent, portable APIs to access all sorts of I/O services with minimum overhead and maximum efficiency. NIO sweeps away many barriers to the adoption of Java where I/O performance is critical, allowing Java to compete on an equal footing with natively compiled languages.
|
Related Reading
|
In this article I'm not going to explain buffers, channels, selectors, and the other denizens of the NIO depths. There just isn't room here to do so properly. My book, Java NIO, does all that. In this space, I'll list some new things you can do with NIO that you couldn't do before in Java. If you need a little context as you go along, visit this page (part of Sun Microsystems' JDK documentation), which gives a brief synopsis and links into the J2SE 1.4 Javadoc.
So, without further ado, in the time-honored tradition of O'Reilly authors flogging their own books by cooking up lame top ten lists: Top Ten New Things You Can Do with NIO That You Couldn't Do Before (TTNTYCDW..., oh never mind).
File locking is one of those things most programmers don't need very often. But for those of you who do need it, you know you simply can't live without it. Prior to NIO, there was no way, short of resorting to native methods, to set or check for file locks in Java applications. File locking is notoriously OS- (and even filesystem-) specific, so the native route is fraught with peril if you need any sort of portability.
With NIO, file locks are built right into the FileChannel
class. It's now easy to create, test, and manage file locks on
any platform that supports file locks at the OS level. File locks are
generally needed when integrating with non-Java applications, to
mediate access to shared data files. In Figures 1 and 2 (borrowed from
my book) assume
the writer process is a legacy application that can't be
replaced. With NIO, new reader applications can be written in Java that use
the same locking conventions to seamlessly integrate with the
pre-existing, non-Java application.

Figure 1: Reader processes holding shared locks.

Figure 2: Writer process holding exclusive lock.
File locks are generally not appropriate for intra-JVM coordination between threads; they operate at the file and process levels. The OS doesn't usually differentiate between threads within a process for lock ownership purposes. That means all threads own all locks equally within a JVM. File locks are primarily needed when integrating with non-Java applications, or between distinct JVMs.
You may never need to use file locks, but with NIO, now you have the option. Adding file-based locking to the Java bag of tricks further eliminates barriers to adoption of Java in the enterprise, especially where it's necessary to work and play well with others.
Regular expressions (java.util.regex) are part of NIO.
I know, they're neither "new" nor "I/O," but a
standardized regular expression library was mandated as part of JSR 51, so there you go.
Regular expressions are not new in Java (several add-on packages have been around for quite some time) but now they're built right into the base J2SE distribution. According to Jeffrey E. F. Friedl's recently updated Mastering Regular Expressions book, the regex engine in J2SE 1.4 is the fastest and best of the lot -- good to know.
One nice side effect of having a regular expression engine
integrated into the base JDK is that other base classes can make use
of it. In J2SE 1.4, the String class has been extended to
be regex-aware by adding the following new methods:
package java.lang;
public final class String
implements java.io.Serializable, Comparable, CharSequence
{
// This is a partial API listing
public boolean matches (String regex)
public String [] split (String regex)
public String [] split (String regex, int limit)
public String replaceFirst (String regex, String replacement)
public String replaceAll (String regex, String replacement)
}
These methods are useful because you can invoke them directly
on a string you're working with. For example, rather than
instantiating Pattern and Matcher objects,
invoking methods on them, and checking the result, you can do simple
tests like this, which are less error-prone and communicate better:
public static final String VALID_EMAIL_PATTERN =
"([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]"
+ "{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))"
+ "([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)";
...
if (emailAddress.matches (VALID_EMAIL_PATTERN)) {
addEmailAddress (emailAddress);
} else {
throw new IllegalArgumentException (emailAddress);
}
The split() method is also handy, especially in
cases where you'd normally use the StringTokenizer
class. It has two advantages over StringTokenizer; it
applies a (potentially sophisticated) regular expression to the target
string to break it into tokens and it does the parsing all in one
shot. Rather than writing yet another tokenizing loop, you can just
do:
String [] tokens = lineBuffer.split ("\\s*,\\s*");
This splits the string lineBuffer (which contains a
series of comma-separated values) into substrings and returns those
strings in a type-safe array. This regular expression allows zero or
more whitespace characters before and/or after each comma. You can
also limit the number of times the string is split, in which case the
last string in the array will be the remainder of the input string.
|
NIO introduces buffers, a gaggle of related classes in the
java.nio package (see Figure 3). Upon first impression,
buffers seem like something you were assigned in Computer Science 101
class. They're very simple objects that encapsulate a fixed-size
array of primitive values, along with some state information about the
array. That's pretty much it.

Figure 3: Buffer class family tree.
Buffers were created primarily to act as containers for data being
sent to or received from channels. Channels are conduits to low-level
I/O services and are always byte-oriented; they only know how to use
ByteBuffer objects.
So what do we use the other buffers types for? Instances of the non-byte
buffers can be created from scratch, or wrapped around an array of the
appropriate type. They can be useful that way, but such a buffer
cannot be used for I/O. However, there's a third way to create
non-byte buffers, as a view of an existing
ByteBuffer.
For example, let's say you have a file containing Unicode
characters stored as 16-bit values (this is the UTF-16 encoding, not
the UTF-8 encoding used for common text files). If you were to read a
chunk of this file into the byte buffer, you could then create a
CharBuffer view of those bytes, like this:
CharBuffer charBuffer = byteBuffer.asCharBuffer();
This creates a view of the original ByteBuffer, which
behaves like a CharBuffer, combining each pair of bytes
in the buffer into a 16-bit char value, as represented by
Figure 4 (this figure shows that odd remaining bytes are not included
in the view; let's assume here that you started with an even-size byte
buffer).

Figure 4: CharBuffer view of a ByteBuffer.
You can then use the CharBuffer object to iterate over
the data (using relative get() calls), access it randomly
with absolute get()s, or copy the data to a char[]
array and pass it along to another object that knows nothing about
buffers.
The ByteBuffer class also has methods to do ad hoc
accesses of individual primitive values. For example, to access four
bytes of a buffer as an int, you could do the following:
int fileSize = byteBuffer.getInt();
This extracts four bytes from the buffer, beginning at the current
position (there are also absolute versions of these methods) and
combines them to form a 32-bit int value. A very cool
thing about this is that the four bytes need not be aligned on any
particular address boundaries. The ByteBuffer
implementation will do whatever it needs to do to assemble the bytes
(or disassemble, for put()) if the underlying hardware
does not permit misaligned memory accesses.
If you've ever had to deal with cross-platform issues,
you're probably wondering at this point about byte order in the
previous example. The CharBuffer view groups pairs of
bytes together to form 16-bit values, but which byte is high and which
one is low? The order in which bytes are combined to form larger
numeric values is known as endian-ness. When the numerically-most-significant byte is stored first in memory (at the lower
address), this is big-endian byte order (see Figure 5). The
opposite, where the least significant byte occurs first, is little-endian
(see Figure 6).

Figure 5: Big-endian.

Figure 6: Little-endian.
In the example in the previous section, are the 16-bit Unicode chars stored as little-endian (UTF-16LE) or big-endian (UTF-16BE)? They could be stored in the file either way, so we need a means of controlling how the view buffer maps the bytes to chars.
Every buffer object has a byte order setting. For all but
ByteBuffer, this is a read-only property and cannot be
changed. The byte order setting of ByteBuffer objects can
be changed at any time. Doing so affects the resulting byte order of
any views created of that ByteBuffer object. So, if we
knew that the Unicode data in our file was encoded as UTF-16LE
(little-endian), we'd set the ByteBuffer's byte
order prior to creating the view CharBuffer, thusly:
byteBuffer.order (ByteOrder.LITTLE_ENDIAN);
CharBuffer charBuffer = byteBuffer.asCharBuffer();
The new view buffer inherits the byte order setting of the
ByteBuffer. Subsequent changes to the ByteBuffer's
byte order will not affect that of the view. The initial byte order
setting of a ByteBuffer object is always big-endian,
regardless of the native byte ordering of the hardware it's
running on.
What if we didn't know the byte order of the Unicode data in
the file? If the file was encoded with the portable UTF-16 encoding,
the first two bytes of the file would contain a byte order marker
value (if it's directly encoded as UTF-16LE or UTF-16BE, then you
need prior knowledge of the byte order). If you were to test that byte
order marker, you could set the byte order appropriately before
creating the CharBuffer view.
A ByteBuffer object's current byte order setting
also affects byte swabbing for data element views (getInt(),
getLong(), getFloat(), etc.). The
buffer's byte order setting at the time of the call affects how
bytes are combined to form the return value or broken out for storage
in the buffer.
The data elements encapsulated by a buffer can be stored in one of
several different ways: in a private array created by the buffer
object (allocation), in an array you provide (wrapping), or, in the
case of direct buffers, in native memory space outside of the JVM's
memory heap. When you create a direct buffer (by invoking
ByteBuffer.allocateDirect()), native system memory is
allocated and a buffer object is wrapped around it.
The primary purpose of direct buffers is for doing I/O on channels. Channel implementations can set up OS-level I/O operations to act directly upon a direct buffer's native memory space. That alone is a powerful new capability and a key to NIO's efficiency. But those I/O operations occur under the hood; they're not something you can use directly. But there is an aspect of direct buffers that you can exploit to great advantage.
The ability to use native memory to hold buffer data is enabled by some new JNI methods, which make it possible, for the first time, for a Java object to access memory space allocated in native code. Prior to 1.4, native code could access data in the JVM heap (if it was very careful -- there were severe restrictions), but Java code could not reach memory allocated by native code.
Now, not only can JNI code discover the address of the native
memory space inside of a buffer created with ByteBuffer.allocateDirect()
on the Java side, but it can allocate its own memory (with malloc(),
for example) and then call back to the JVM to wrap that memory space
in a new ByteBuffer object (the JNI method to do this is
NewDirectByteBuffer()).
The really exciting part is that a ByteBuffer object
can be wrapped around any memory address the native code can
obtain, even memory outside the JVM's own address space. One
example is creating a direct ByteBuffer object that
encapsulates the memory on a video card. Such an buffer enables pure
Java code to read and write directly to video memory with no system
calls or buffer copies. Pure Java video drivers! All you need is a
tiny bit of JNI glue to grab the video memory and return a
ByteBuffer object. You couldn't do that before NIO
came along.
|
The theme of wrapping ByteBuffer objects around
arbitrary memory spaces continues with MappedByteBuffer,
a specialized form of ByteBuffer. On most operating
systems, it's possible to memory map a file using the
mmap() system call (or something similar) on an open file
descriptor. Calling mmap() returns a pointer to a memory
segment, which actually represents the content of the file. Fetches
from memory locations within that memory area will return data from
the file at the corresponding offset. Modifications made to the memory
space are written to the file on disk.

Figure 7: User memory mapped to the filesystem.
There are two big advantages to memory mapped files. First, the "memory" does not usually consume normal virtual memory space. Or, more correctly, the virtual memory space of a file mapping is backed by the file data on disk. That means it's not necessary to allocate regular paging space for mapped files; their paging area is the file itself. If you were to open the file conventionally and read it into memory, that would consume a corresponding amount of paging space, because you're copying the data into regular memory. Second, multiple mappings of the same file share the same virtual address space. Theoretically, 100 mappings can be established by 100 different processes to the same 500MB file; each will appear to have the entire 500MB of data in memory, but the overall memory consumption of the system won't change a bit. Pieces of the file will be brought into memory as references are made, which will compete for RAM, but no paging space will be consumed.
In Figure 7, additional processes running in user space would map to that same physical memory space, through the same filesystem cache and thence to the same file data on disk. Each of those processes would see changes made by any other. This can be exploited as a form of persistent, shared memory. Operating systems vary in the way their virtual memory subsystems behave, so your mileage may also vary.
MappedByteBuffer instances are created by invoking the
map() method on an open FileChannel object.
The MappedByteBuffer class has a couple of additional
methods for managing caching and flushing of updates to the underlying
file.
Prior to NIO, it wasn't possible to memory map files without resorting to platform-specific, non-portable native code. It's now possible for any pure Java program to take advantage of memory mapping, easily and portably.
Here's a familiar bit of code:
byte [] byteArray = new byte [100];
...
int bytesRead = fileInputStream.read (byteArray);
This reads some data from a stream into an array of bytes.
Here's the equivalent read operation using ByteBuffer
and FileChannel objects (to move the examples into the
NIO realm):
ByteBuffer byteBuffer = ByteBuffer.allocate (100);
...
int bytesRead = fileChannel.read (byteBuffer);
And here's a common usage pattern:
ByteBuffer header = ByteBuffer.allocate (32);
ByteBuffer colorMap = ByteBuffer (256 * 3)
ByteBuffer imageBody = ByteBuffer (640 * 480);
fileChannel.read (header);
fileChannel.read (colorMap);
fileChannel.read (imageBody);
This performs three separate read() calls to load a
hypothetical image file. This works fine, but wouldn't it be great
if we could issue a single read request to the channel and tell it to
place the first 32 bytes into the header buffer, the next
768 bytes into the colorMap buffer, and the remainder into
imageBody?
No problem, can do, easy. Most NIO channel types support scatter/gather, also known as vectored I/O. A scattering read to the above buffers can be accomplished with this code:
ByteBuffer [] scatterBuffers = { header, colorMap, imageBody };
fileChannel.read (scatterBuffers);
Rather than pass a single buffer object to the channel, an array of buffers is passed in. The channel fills each buffer in turn until all are full or there're no more data to read. Gathering writes are done in a similar way; data are drained from each buffer in the list in turn and sent along the channel, exactly as if they had been written sequentially.
Scatter/gather can provide a real performance boost when reading or writing data that's partitioned into fixed-size, logically distinct segments. Passing a list of buffers means the entire transfer can be optimized (using multiple CPUs for example) and fewer overall system calls are needed.
Gathering writes can compose results from several buffers. For
example, an HTTP response could use a read-only buffer containing
static headers that are the same for every response, a dynamically-populated buffer for those headers unique to the response, and a
MappedByteBuffer object associated with a file, which is
to be the body of the response. A given buffer may even appear in more
than one gather list, or multiple views of the same buffer can be
used.
Did you even notice that whenever you need to copy data to or from a file, you seem to write the same old copy loop over and over again? It's always the same story: you read a chunk of data into a buffer then immediately write it back out again somewhere else. You're not doing anything with that data so why is it necessary to pull it in just to shove it back out again? Why is it necessary to continually reinvent this wheel?
Here's a thought. Wouldn't it be great if you could just tell some class "Move the data from this file to that one" or "Write all the data that comes out of that socket to this file over there"? Well, thanks to the modern miracle of direct channel transfers, now you can.
public abstract class FileChannel
extends AbstractChannel
implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
{
// This is a partial API listing
public abstract long transferTo (long position, long count,
WritableByteChannel target)
public abstract long transferFrom (ReadableByteChannel src,
long position, long count)
}
A channel transfer lets you cross-connect two channels so that data
is transfered directly from one to the other without any further
intervention on your part. Because the transferTo() and
transferFrom() methods belong to the FileChannel
class, a FileChannel object must be the source or
destination of a channel transfer (you can't transfer from one
socket to another, for example). But the other end may be any
ReadableByteChannel or WritableByteChannel,
as appropriate.
On operating systems with appropriate support, channel transfers can be done entirely in kernel space. This not only relieves you of the chore of doing the copy, it bypasses the JVM entirely! One low-level system call and boom! Done. Even on those OS platforms without kernel support for transfers, making use of these methods still saves you the trouble of writing yet another copy loop. And the odds are good that the implementation will use native code or other optimizations to move the data as quickly as possible, faster than you could ever do it yourself in regular Java code. And the best part: code you never write never has bugs.
|
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.
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.
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.
Copyright © 2009 O'Reilly Media, Inc.