Channels are similar to streams available for blocking IO with a few differences:

  • You can both read and write to Channels. Streams are typically one-way (read or write).
  • Channels can be read and written asynchronously.
  • Channels always read to, or write from, a Buffer.

The Channels have multiple implementations depending on the data to be read or written:

  • FileChannel: Used to read and write data from and to the files
  • DatagramChannel: Used for data exchange over network using UDP packets
  • SocketChannel: TCP channel to exchange data over TCP sockets
  • ServerSocketChannel: An implementation similar to a web server listening to requests over a specific TCP port. It creates a new SocketChannel instance for every new connection

This post will take FileChannel for further exploration, from example to implementation.

Basic Channel Example

Here is a basic example that uses a FileChannel to read some data into a Buffer:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {

  System.out.println("Read " + bytesRead);
  buf.flip();

  while(buf.hasRemaining()){
      System.out.print((char) 
      buf.get());
  }

  buf.clear();
  bytesRead = inChannel.read(buf);
}
aFile.close();

Java NIO supports built-in scatter and gather.

  • A “scattering read” reads data channel into more than one buffer. Here is a code example and an illustration.
  • A “gathering write” writes data from more than one buffer into a single channel.

For more explanation about scatter and gather, see Java NIO Scatter / Gather . In this post, I consider the interaction between one channel and one buffer only for briefness.

Opening a FileChannel

FileChannel allows you read from or write to a file. A FileChannel cannot be non-blocking which upsets you.

You cannot open FileChannel directly, but can obtain a FileChannel via a RandomAccessFile, InputStream or OutputStream. From the above basic example, we can see how RandomAccessFile open a FileChannel.

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

What actually RandomAccessFile does lists as follows,

// RandomAccessFile.java

public final FileChannel getChannel() {
    synchronized (this) {
        if (channel == null) {
            channel = FileChannelImpl.open(fd, path, true, rw, this);
        }
        return channel;
    }
}

After jumping to FileChannelImpl, open() method justs construct an instance of FileChannelImpl.

public static FileChannel open(FileDescriptor fd, String path,
                               boolean readable, boolean writable,
                               Object parent)
{
    return new FileChannelImpl(fd, path, readable, writable, false, parent);
}

private FileChannelImpl(FileDescriptor fd, String path, boolean readable,
                        boolean writable, boolean direct, Object parent)
{
    this.fd = fd;
    this.readable = readable;
    this.writable = writable;
    this.parent = parent;
    this.path = path;
    this.direct = direct;
    this.nd = new FileDispatcherImpl();
    if (direct) {
        assert path != null;
        this.alignment = nd.setDirectIO(fd, path);
    } else {
        this.alignment = -1;
    }

    // Register a cleaning action if and only if there is no parent
    // as the parent will take care of closing the file descriptor.
    // FileChannel is used by the LambdaMetaFactory so a lambda cannot
    // be used here hence we use a nested class instead.
    this.closer = parent != null ? null :
        CleanerFactory.cleaner().register(this, new Closer(fd));
}

As to Constructor, let’s the fields involved in FileChannelImpl.

// FileChannelImpl.java

public class FileChannelImpl
    extends FileChannel
{
    // Memory allocation size for mapping buffers
    private static final long allocationGranularity;

    // Access to FileDescriptor internals
    private static final JavaIOFileDescriptorAccess fdAccess =
        SharedSecrets.getJavaIOFileDescriptorAccess();

    // Used to make native read and write calls
    private final FileDispatcher nd;

    // File descriptor
    private final FileDescriptor fd;

    // File access mode (immutable)
    private final boolean writable;
    private final boolean readable;

    // Required to prevent finalization of creating stream (immutable)
    private final Object parent;

    // The path of the referenced file
    // (null if the parent stream is created with a file descriptor)
    private final String path;

    // Thread-safe set of IDs of native threads, for signalling
    private final NativeThreadSet threads = new NativeThreadSet(2);

    // Lock for operations involving position and size
    private final Object positionLock = new Object();

    // Positional-read is not interruptible
    private volatile boolean uninterruptible;

    // DirectIO flag
    private final boolean direct;

    // IO alignment value for DirectIO
    private final int alignment;

    // Cleanable with an action which closes this channel's file descriptor
    private final Cleanable closer;
}

Reading Data from a FileChannel

It’s easy to read data from a FileChannel to a allocated Buffer by calling read() method like this,

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);

The read() underneath tackles read operation with the following steps:

  • ensure this channel is open
  • make certain the file access mode is readable
  • check whether or not the position of channel is aligned
  • marks the beginning of the I/O operation that might block indefinitely.
  • check the buffer is readable
  • allocate a temporary buffer and write data into it
  • assign newborn buffer to destination buffer
  • normalize the status of this I/O operation
// FileChannelImpl.java
public int read(ByteBuffer dst) throws IOException {
    ensureOpen();
    if (!readable)
        throw new NonReadableChannelException();
    synchronized (positionLock) {
        if (direct)
            Util.checkChannelPositionAligned(position(), alignment);
        int n = 0;
        int ti = -1;
        try {
            begin();
            ti = threads.add();
            if (!isOpen())
                return 0;
            do {
                n = IOUtil.read(fd, dst, -1, direct, alignment, nd);
            } while ((n == IOStatus.INTERRUPTED) && isOpen());
            return IOStatus.normalize(n);
        } finally {
            threads.remove(ti);
            end(n > 0);
            assert IOStatus.check(n);
        }
    }
}

// IOUtil.java
static int read(FileDescriptor fd, ByteBuffer dst, long position,
                boolean directIO, int alignment, NativeDispatcher nd)
    throws IOException
{
    if (dst.isReadOnly())
        throw new IllegalArgumentException("Read-only buffer");
    if (dst instanceof DirectBuffer)
        return readIntoNativeBuffer(fd, dst, position,
                directIO, alignment, nd);

    // Substitute a native buffer
    ByteBuffer bb;
    int rem = dst.remaining();
    if (directIO) {
        Util.checkRemainingBufferSizeAligned(rem, alignment);
        bb = Util.getTemporaryAlignedDirectBuffer(rem,
                                                  alignment);
    } else {
        bb = Util.getTemporaryDirectBuffer(rem);
    }
    try {
        int n = readIntoNativeBuffer(fd, bb, position,
                directIO, alignment,nd);
        bb.flip();
        if (n > 0)
            dst.put(bb);
        return n;
    } finally {
        Util.offerFirstTemporaryDirectBuffer(bb);
    }
}

Eventually, the read operation is achieved by readIntoNativeBuffer with the help of NativeDispatcher, which, as its name implies, will call native method of read0 and write0.

// IOUtil.java

private static int readIntoNativeBuffer(FileDescriptor fd, ByteBuffer bb,
                                        long position, boolean directIO,
                                        int alignment, NativeDispatcher nd)
    throws IOException
{
    int pos = bb.position();
    int lim = bb.limit();
    assert (pos <= lim);
    int rem = (pos <= lim ? lim - pos : 0);

    if (directIO) {
        Util.checkBufferPositionAligned(bb, pos, alignment);
        Util.checkRemainingBufferSizeAligned(rem, alignment);
    }

    if (rem == 0)
        return 0;
    int n = 0;
    if (position != -1) {
        n = nd.pread(fd, ((DirectBuffer)bb).address() + pos,
                     rem, position);
    } else {
        n = nd.read(fd, ((DirectBuffer)bb).address() + pos, rem);
    }
    if (n > 0)
        bb.position(pos + n);
    return n;
}

Writing Data to a FileChannel

Writing data to a FileChannel is done using the FileChannel.write() method, which takes a Buffer as parameter. Here is an example.

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}

Because of no guarantee of how many bytes the write() method writes to the FileChannel, write() is called repeatedly until the buffer has no further bytes to write.

The steps of write() of FileChannel is similar with read() concerning position check and native operation.

// FileChannelImpl.java

public int write(ByteBuffer src) throws IOException {
    ensureOpen();
    if (!writable)
        throw new NonWritableChannelException();
    synchronized (positionLock) {
        if (direct)
            Util.checkChannelPositionAligned(position(), alignment);
        int n = 0;
        int ti = -1;
        try {
            begin();
            ti = threads.add();
            if (!isOpen())
                return 0;
            do {
                n = IOUtil.write(fd, src, -1, direct, alignment, nd);
            } while ((n == IOStatus.INTERRUPTED) && isOpen());
            return IOStatus.normalize(n);
        } finally {
            threads.remove(ti);
            end(n > 0);
            assert IOStatus.check(n);
        }
    }
}

Closing a FileChannel

When you are done using a FileChannel you must close it by channel.close(); .
What implCloseChannel does includes:

  • check whether the FileDescriptor is valid
  • release and invalidate any locks that we still hold
  • signal any threads blocked on this channel
  • close FileDescriptor
  • perform the cleaning action
// FileChannelImpl.java

protected void implCloseChannel() throws IOException {
    if (!fd.valid())
        return; // nothing to do

    if (fileLockTable != null) {
        for (FileLock fl: fileLockTable.removeAll()) {
            synchronized (fl) {
                if (fl.isValid()) {
                    nd.release(fd, fl.position(), fl.size());
                    ((FileLockImpl)fl).invalidate();
                }
            }
        }
    }

    threads.signalAndWait();

    if (parent != null) {

        // Close the fd via the parent stream's close method.  The parent
        // will reinvoke our close method, which is defined in the
        // superclass AbstractInterruptibleChannel, but the isOpen logic in
        // that method will prevent this method from being reinvoked.
        //
        ((java.io.Closeable)parent).close();
    } else if (closer != null) {
        // Perform the cleaning action so it is not redone when
        // this channel becomes phantom reachable.
        closer.clean();
    } else {
        fdAccess.close(fd);
    }
}

position() and truncate() are skipped in this post, because they share analogous steps with read() and write().

Reference