http://www.developer.com/

Back to article

An Introduction to Java NIO and NIO.2


September 9, 2009

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:

  1. Buffers - containers for data
  2. Charsets - translations to and from Unicode
  3. Channels - connections to I/O-capable providers
  4. 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:

  //open the files using the old fashioned way -- streams
  FileInputStream fsin = new FileInputStream(srcFileName);
  FileOutputStream fsout = new FileOutputStream(trgtFilename);
  RandonAccesFile raf = new RandomAccessFile(someFilename, "rw")
  
  //retrieve the respective channels using getChannel method
  FileChannel fcin = fsin.getChannel();
  FileChannel fcout = fsout.getChannel();
  FileChannel fcran = raf.getChannel();

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.

  //open the files using the old fashioned way -- streams
  RandomAccessFile rwf = new RandomAccessFile(someFilename, "rw")
  RandomAccessFile rof = new RandomAccessFile(someFilename, "r")
  
  //retrieve the respective channels using getChannel method
  FileChannel fchrw = rwf.getChannel();
  FileChannel fchr  = rof.getChannel();
  
  //map the file to the buffer -- creates read-write buffer
  MappedByteBuffer mbb = fchrw.map(FileChannel.MapMode.READ_WRITE, 0, fc.size);
  //map the file to the buffer -- creates read-only buffer
  MappedByteBuffer mbb = fchrw.map(FileChannel.MapMode.READ_WRITE, 0, fc.size);
  
  //do any data translation needed since MappedByteBuffer extends ByteBuffer

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:

  //open the files using the old fashioned way -- streams
  File inFile = new File("C:/SourceFile.java"); // pick some arbitrary java file 
  File transferFile = new File("C:/TransferFile.txt");
  File copyFile = new File("C:/SourceFileCopy.txt ");
  
  //retrieve the respective channels using getChannel method
  FileChannel fcin = new RandomAccessFile(inFile, "r").getChannel();
  FileChannel fcoutTransfer = new RandomAccessFile(transferFile, "rw").getChannel();
  FileChannel fcoutCopy = new RandomAccessFile(copyFile, "rw").getChannel();
  
  //use transferTo method to copy 
  Long bytesTransferred = fcin.transferTo(0, fcin.size(), fcoutTransfer);
  
  //allocate buffer -- use the right size by experimenting with different sizes
  ByteBuffer buffer = ByteBuffer.allocateDirect(1024); //direct buffer
  
  long size = fcin.size(), n=0;
  
  //time to play the old game - clear, read, flip and write
  while( n<size ) {
   buffer.clear(); //makes the buffer ready by resetting the pointers
   if( fcin.read(buffer) < 0 ) //fill the buffer by reading from channel
    break;
   buffer.flip(); //makes the buffer writing the data just read
   n+= fcout.write(buffer);
  }
  //close the resources
  fcin.close(); fcoutTransfer.close();fcoutCopy.close();

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:

  • multiple threads for polling devices for readiness
  • required dedicating a thread to each socket connection
  • blocking until data is available

  class Server implements Runnable {
    public void run() {
    try {
       ServerSocket ss = new ServerSocket(PORT);
       while (!Thread.interrupted())
        new Thread(new Handler(ss.accept())).start();
        // one thread per socket connection
   // every thread created this way will essentially block for I/O
    } catch (IOException ex) { /* ... */ }
  }

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.


Figure 3. Selector Topology

Some notes about non-blocking I/O:

  • In non-blocking mode, an I/O operation never blocks (methods return immediately),
  • 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.
  • Capable of connecting (using its associated java.net.Socket), reading and writing (using ByteBuffer).
  • Extends SelectableChannel (discussed below) to support non-blocking connections so that it could be multiplexed via a Selector (discussed below).
  • Is a Select-able channel for stream-oriented connecting sockets

Creating a SocketChannel

  //open the channel to create it 
  SocketChannel socketChannel = SocketChannel.open();
      
  //create and prepare a buffer for data-transfer (read and ByteBuffer buffer = ByteBuffer.allocate(1000);
  
  //try connecting to the server
  socketChannel.connect(new InetSocketAddress(host, port));
  
  //read the response - prepare to deal with partial reads
  buffer.clear();
  for (;;) {
   if (in.read(buffer) < 0 && !buffer.hasRemaining())
     break;        // No more bytes to transfer
  buffer.flip();
  //process the request
  out.write(buffer)
  buffer.compact();    // In case of partial write
  }

DatagramChannel

  • A partial abstraction of network UDP (datagram).
  • Capable of connecting (using its associated java.net.DatagramSocket), reading and writing (using ByteBuffer).
  • Is a Select-able channel for datagram-oriented Sockets.

Creating a DatagramChannel

  //open the channel to create it
  DatagramChannel datagramChannel = DatagramChannel.open();
  
  //set the non-blocking mode on 
  datagramChannel.configureBlocking(false);
        
  //try connecting to the socket
  datagramChannel.connect(new InetSocketAddress(host, port))

ServerSocketChannel

  • A partial abstraction of stream-oriented Sockets.
  • 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.

References

Monitoring Direct Buffers
NIO Java Specification
Article on Concurrent processing

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date