An Introduction to Java NIO and NIO.2

Introduction

In this article we will review some of the existing features of the java.nio (New I/O) package that are a part of Java v1.4, v1.5 and v1.6.

The java.nio package provides several useful abstractions:

Buffers - containers for data

Charsets - translations to and from Unicode

Channels - connections to I/O-capable providers

java.nio.channels.spi - multiplexed, non-blocking channel I/O

We will focus on Buffers and Channels, designed to provide bulk data transfer, multiplexing, and non-blocking IO.

java.nio.Buffer

NIO buffer is a container for a finite amount of data that is held in contiguous memory blocks in a linear fashion. Each sub class of a Buffer represents a specific Java non-boolean primitive type with ByteBuffer being the one most commonly used. The NIO buffer facilitates bulk data transfers data transfers between Channels by eliminating the need for any additional copying or auxiliary data structures.

A Buffer has the following properties:

position: the index of the next element to be read or written. A sequence of get() or put() operations with no arguments are always conducted relative to the current position (incrementing the position by one). Absolute get(n) and put(n) operations take an explicit element index and have no bearing on the position.

capacity: the maximum number of elements a Buffer can contain. A Buffer is instantiated with a specific capacity which cannot be changed.

limit: index of the first element that should not be read or written (anything beyond the limit is a forbidden access)--useful for partial access as demonstrated in the example below. Relative get or put operation on the same buffer is successful until position < limit.

mark: marks stores the current buffer position when you call mark() and reset() sets the position to the previously-marked position (position=mark).

These properties are never negative and follow the invariant:

0 <= mark <= position <= limit <=capacity

ByteBuffer

A container for holding binary data with convenience methods to compact, duplicate and slice the data contents.

ByteBuffer buffer = ByteBuffer.allocate(7);

Figure 1. Newly created ByteBuffer

Direct and Non-Direct

The NIO ByteBuffer has two flavors: direct (non-heap buffers) and non-direct (heap buffers). The contents of a direct buffer are allocated from the host operating system instead of the Java heap, and are not managed by the Java garbage collector. Non-direct buffers are copied into direct buffers for native I/O operations.

The JDK tool jconsole can be used to monitor the resources associated with direct ByteBuffers. [ONLY STARTING WITH Java 1.7]

Some notes about direct buffers from the JDK documentation are in order:

"It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system's native I/O operations. In general it is best to allocate direct buffers only when they yield a measurable gain in program performance."

Allocation

A direct buffer is created using either the ByteBuffer.allocateDirect(), or the FileChannel.map() method that maps a region of a file directly into memory.

A non-direct buffer is created using the ByteBuffer methods allocate() or wrap(). The wrap() method converts an existing byte array into a ByteBuffer. The underlying array is always available by invoking the
array() method.

Byte-Order

The byte-order of a newly-created byte buffer is always BIG_ENDIAN -- where bytes of a multibyte value are ordered from least significant to most significant. You can query the order by calling order()or change the order by calling order(ByteOrder). This is a very useful feature if you are dealing with legacy data stored in binary files generated by programs in other languages on varying hardware.

Using ByteBuffer

Let's look at some of the important methods of a ByteBuffer:

wrap(byte[]) Converts any byte array into a ByteBuffer. Any modifications to the ByteBuffer also change the underlying array.

slice() creates a new ByteBuffer with a capacity and the limit equal to the number of bytes remaining (limit-position) in the source buffer with mark undefined. Both the buffers share the content and hence the changes to one of the buffer are visible in other.

compact() In the case of non- blocking I/O, there is always a possibility of a partial read or write, where only a part of a buffer was read or written. Before the next I/O operation, the data that was already used has to be drained so that only the remaining data which was not transferred/transmitted remains in the ByteBuffer. This method achieves exactly that.

asReadOnlyBuffer(): creates a read- only version to share the contents of the source buffer. The new buffer (read-only) created doesn't allow the contents to be modified. However, changes to the source buffer's content will be visible in the read-only buffer.

WARNING: Not Thread-Safe

None of the Buffer or ByteBuffer methods is thread safe. Be sure to use the appropriate locking (synchronization) methods when they are used in multi-threaded implementations.

Channel

A channel is a data conduit that facilitates data transfer using ByteBuffer. It represents connectivity to an entity capable of performing I/O operations, such as files, network sockets etc. While stream I/O reads a character at a time, channel I/O reads a buffer at a time. It is important to size the ByteBuffer appropriately to the underlying operating system block size for optimum performance.

A channel is bidirectional--you can read from a channel (ReadableByteChannel) or write to a channel(WritableBytechannel).

The interface java.nio.channels.Channel has only two methods:

isOpen() checks if the channel is open-- until JDK 1.6--the Files are opened using streams to fetch the channel, in lieu of an open() method in a channel.

close() closes the channel--blocking happens when more than one thread attempts to close it.

We will look at some of the important subclasses of a Channel:

FileChannel

A FileChannel is a specialization for reading, writing and manipulating a file (directory operations).

The following are some of the capabilities of a FileChannel:

Mapping a region of a file directly into memory (efficient for large files)

Interacting with file system locks

Transferring bytes between the channels more efficiently without the application code having to use buffers and looping.

The following UML shows the most significant methods excluding scattering and gathering behavior.

Figure 2. FileChannel

Since there is no open() method in FileChannel to open a channel (prior to NIO.2), you could use Java I/O streams to associate a file channel as shown in the example below:

A FileChannel is capable of transferring bytes between the channels directly or when marshaled within a ByteBuffer. It is important to size the ByteBuffer appropriately to the underlying operating system block size for optimum performance.

Let's look at some of the important methods of a FileChannel:

position() Similar to a Buffer, FileChannel also has a file position that marks the current position from the beginning of the file. A read or write operation on this file updates the position.

position(newPosition) Slides the file position similar to RandomAccessFile.seek(). You could set the position greater than the file's current size, but it does not change the size of the file, unless you write to the file at the new position.

truncate(newSize) Shrinks the file-- if the given size is greater than or equal to the file's size then the file is not modified. Otherwise the file length is set to the new size.

transferTo() Copies bytes from the channel the file is associated with to a given writeable channel directly without the use of buffers.

transferFrom() Copies bytes into the channel the file is associated with from a given readable channel directly without the use of buffers.

The behavior of the transferTo()/transferFrom is dependent upon the host operating system, as noted in the Javadoc entry:

"Many operating systems can transfer bytes directly from the source channel into the filesystem cache without actually copying them."

map (FileChannel.MapMode, position, size) A convenient way to map a region of the channel's data file into a direct byte buffer (MappedByteBuffer) so that you could change the byte order, access file data, or translate the data using methods like asIntBuffer().

The MappedByteBuffer returned by this method will have a position of zero and a limit and capacity of size; its mark will be undefined.

I/O from MappedByteBuffer implies fetching bytes from the file and writing the buffer stores bytes directly into the file, avoiding system call overhead or buffer copies.

read(ByteBuffer) copies bytes from the file into the buffer. The file position is updated with number of bytes read.

write(ByteBuffer) copies bytes from the buffer to the file. The file position is updated with the number of bytes written. If the file is in append mode, the position is first advanced to the end of the file before writing to the file.

The following is an example of read/write methods that copy files using a direct buffer as well as transferTo() methods:

lock() This method blocks until an exclusive lock is acquired, returning a FileLock object. If the channel is closed, or the invoking thread is interrupted, this method gives up trying to acquire the lock and throws the appropriate exception. Regions of a file can be blocked by calling lock(position, size, shared), whose parameters specify the position at which the locked region is to start, the size of the locked region and whether the lock is shared or exclusive.

The behavior of the file lock is dependent upon the host operating system, as noted in the Javadoc entry:

"Some operating systems do not support shared locks, in which case a request for a shared lock is automatically converted into a request for an exclusive lock . File locks are held on behalf of the entire Java virtual machine. They are not suitable for controlling access to a file by multiple threads within the same virtual machine".

//open the files using the old fashioned way -- use File object
File file = new File(filename);
//retrieve the respective channels using getChannel method
FileChannel fcin = new RandomAccessFile(file, "rw").getChannel();
//lock the file
FileLock lock = cin.lock(); // this method blocks until it can fetch the lock
lock.release();
lock = cin.tryLock(0, file.size(), true);//try acquiring the lock without blocking
lock.isShared(); //if the lock is shared or exclusive

Non-Blocking I/O

Prior to JDK1.4, threads engaged in network I/O exhibited the following characteristics:

With functionality introduced in JSR-51, it is possible to set Channels in a non-blocking mode, under the watch of a Selector that has the ability to recognize/sense when one or more channels become available for data transfer. This frees the application from having to dedicate threads for blocking on devices awaiting data or implementing expensive polling processes. The following UML diagram shows the significant methods of Selector, SelectableChannel and SelectionKey.

In non -blocking mode, an I/O operation may transfer fewer bytes than were requested (partial read or write) or possibly no bytes at all.

FileChannel does not support non-blocking file operations.

Non-Blocking API is not the same as asynchronous API (JSR-203 - part of JDK 1.7).

Channels that support Network I/O

A socket is conceptually an endpoint for communication between two machines. Below is a UML of the java.nio classes showing significant methods that support network I/O (excluding scattering reads and gathering writes).

Figure 4. Network Channels

SelectableChannel

A channel that can be asynchronously closed and interrupted. Only SelectableChannels can be multiplexed /managed with Selectors.

The following methods provide for the configuration and management of SelectableChannels:

register() creates a SelectionKey as a final product of performing the registration with a given Selector. It takes a Selector Object, interest set for Selector to monitor and an optional attachment as parameters.

configureBlocking() Sets the blocking mode on/off. Choose this method carefully--for all practical purposes, you only call this method once to initiate the non-blocking behavior.

validOps() An integer containing the operation set bitmap.

keyFor(Selector) Retrieves the SelectionKey representing the channel's registration with the given selector.

SocketChannel

A partial abstraction of TCP sockets

Contains an associated java.net.Socket used for binding and shutdown operations.

ServerSocketChannel

Contains an associated java.net.ServerSocket that could be used for binding operations.

" Is a Select-able channel for stream-oriented Sockets with support for non-blocking connections so that it could be multiplexed via a Selector

Creating a non-blocking ServerSocketChannel

//open the channel first before accepting the connections
ServerSocketChannel serSocketChannel = ServerSocketChannel.open();
//you could bind using the associated ServerSocket
serSocketChanne1.socket().bind(new InetSocketAddress(port));
//set the non-blocking mode on
serSocketChannel.configureBlocking(false);

Thanks for reading and stop in next week where we'll dig deep into the new java.nio (New I/O) package.