An Introduction to Java NIO and NIO.2
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.
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
put()operations with no arguments are always conducted relative to the current position (incrementing the position by one). Absolute
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
putoperation on the same buffer is successful until position
- mark: marks stores the current buffer position when you call
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
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."
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
wrap() method converts an existing byte array into a ByteBuffer. The underlying array is always available by invoking the
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.
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.
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).
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: