☕ Java

Channels

A channel in Java NIO is an open connection to an I/O entity — a file, a socket, a pipe — that can be read from, written to, or both. Channels differ from streams in being bidirectional (a single FileChannel can both read and write), operating exclusively with ByteBuffers rather than individual bytes, supporting seeking and position-based access (FileChannel), and being capable of non-blocking mode (network channels). The channel hierarchy descends from java.nio.channels.Channel at the root; ReadableByteChannel and WritableByteChannel define the reading and writing contracts; ByteChannel combines both; SeekableByteChannel adds position and size access; FileChannel extends SeekableByteChannel with file-specific operations including memory mapping, file locking, and zero-copy transfers. For network I/O, SocketChannel, ServerSocketChannel, and DatagramChannel support both blocking and non-blocking modes and are registered with a Selector for multiplexed event-driven I/O. This entry covers the complete FileChannel API including map(), lock(), transferTo(), and transferFrom(); the blocking and non-blocking network channels; the Selector and SelectionKey model; Pipe channels for inter-thread communication; and the AsynchronousChannel group for callback-based or Future-based I/O.

FileChannel — Random Access, Transfers, Memory Mapping, and Locking

FileChannel is the NIO channel for file I/O, obtained via FileChannel.open(Path, OpenOption...) (NIO.2 style, preferred), or from FileInputStream.getChannel(), FileOutputStream.getChannel(), or RandomAccessFile.getChannel() (legacy style). FileChannel is always in blocking mode — FileChannel.configureBlocking(false) throws UnsupportedOperationException because the OS file I/O API does not support true non-blocking file access (asynchronous file channels do this differently). FileChannel implements SeekableByteChannel, providing position() to query or set the current read/write position, size() to query the file size, and truncate(long size) to truncate the file to the specified size. read(ByteBuffer dst) reads bytes from the current position into dst and advances the position by the bytes read. read(ByteBuffer dst, long position) reads from the specified position without changing the channel's current position — this is the random-access read. write(ByteBuffer src) writes bytes from src at the current position. write(ByteBuffer src, long position) writes at the specified position. transferTo(long position, long count, WritableByteChannel target) copies bytes from this channel to a target channel. On Linux, this maps to the sendfile() system call when the target is a SocketChannel: the kernel copies data between file descriptor and socket without any user-space copy — zero-copy transfer. This is the most efficient way to serve file content over a network. transferFrom(ReadableByteChannel src, long position, long count) is the inverse. map(FileChannel.MapMode mode, long position, long size) creates a MappedByteBuffer — a ByteBuffer backed by a memory-mapped region of the file. Reading and writing the MappedByteBuffer accesses the file content directly through virtual memory pages, with page faults bringing in data as needed. The three modes are READ_ONLY (file changes made by other processes are visible; writes to the buffer throw ReadOnlyBufferException), READ_WRITE (writes to the buffer are reflected in the file and vice versa), and PRIVATE (writes create a copy-on-write local copy, not visible in the file or to other processes). lock() and tryLock() acquire an advisory file lock. lock(position, size, shared) acquires a lock on a byte range — the entire file (0, Long.MAX_VALUE, false) for an exclusive lock, or a range for fine-grained locking. shared locks allow multiple readers; exclusive locks exclude all other lockers. File locks are advisory on most OS platforms — they are visible to processes that check for them, but processes that don't use file locking can still read and write the locked file. File locks are released when the channel is closed, the FileLock.release() is called, or the JVM exits.
Java
// ── FileChannel.open — preferred NIO.2 entry point ───────────────────
// Read-only:
try (FileChannel fc = FileChannel.open(Path.of("data.bin"),
        StandardOpenOption.READ)) {
    ByteBuffer buf = ByteBuffer.allocate(1024);
    while (fc.read(buf) != -1) {
        buf.flip();
        processBuf(buf);
        buf.clear();
    }
}

// Read-write, create if missing:
try (FileChannel fc = FileChannel.open(Path.of("store.bin"),
        StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
    // Random access:
    ByteBuffer record = ByteBuffer.allocate(64);
    fc.read(record, 5 * 64);   // read record at index 5 without changing position
    record.flip();
    int id = record.getInt();   // parse record
}

// ── Position, size, truncate ──────────────────────────────────────────
try (FileChannel fc = FileChannel.open(Path.of("data.bin"),
        StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {

    System.out.println("Size: " + fc.size());         // file size in bytes
    System.out.println("Position: " + fc.position()); // current read/write position

    fc.position(1024);   // seek to byte 1024
    ByteBuffer buf = ByteBuffer.allocate(512);
    int n = fc.read(buf);   // read 512 bytes from position 1024

    fc.position(fc.size());  // seek to end
    buf.clear().putLong(System.currentTimeMillis()).flip();
    fc.write(buf);           // append 8 bytes at end

    fc.truncate(4096);       // truncate to 4096 bytes
    System.out.println("After truncate: " + fc.size()); // 4096
}

// ── transferTo — zero-copy file serving ──────────────────────────────
try (FileChannel fileChannel = FileChannel.open(Path.of("file.html"));
     SocketChannel socket    = SocketChannel.open(new InetSocketAddress("host", 80))) {

    long size = fileChannel.size();
    long transferred = 0;
    while (transferred < size) {
        // Loop because transferTo may transfer less than requested:
        transferred += fileChannel.transferTo(transferred, size - transferred, socket);
    }
    System.out.printf("Transferred %,d bytes (zero-copy)%n", transferred);
}

// ── Memory-mapped I/O ─────────────────────────────────────────────────
try (FileChannel fc = FileChannel.open(Path.of("database.bin"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {

    // Map the entire file into virtual memory:
    MappedByteBuffer mapped = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size());
    mapped.order(ByteOrder.LITTLE_ENDIAN);   // set byte order

    // Read from virtual memory — no system call for cached pages:
    mapped.position(0);
    int magic    = mapped.getInt();
    int version  = mapped.getShort() & 0xFFFF;
    System.out.printf("Magic: 0x%X  Version: %d%n", magic, version);

    // Write to virtual memory — page becomes dirty, OS flushes to disk:
    mapped.position(8);
    mapped.putLong(System.currentTimeMillis());  // update timestamp in-place

    // Force dirty pages to disk (equivalent to msync()):
    mapped.force();
}

// ── File locking ──────────────────────────────────────────────────────
try (FileChannel fc = FileChannel.open(Path.of("shared.db"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {

    // Exclusive lock on entire file (blocking — waits until acquired):
    try (FileLock lock = fc.lock()) {
        System.out.println("Exclusive lock acquired");
        // Only this process can hold an exclusive lock:
        performExclusiveOperation(fc);
    }   // lock.release() called by close()

    // Shared lock — multiple readers can hold simultaneously:
    try (FileLock sharedLock = fc.lock(0, Long.MAX_VALUE, true)) {
        System.out.println("Shared lock acquired");
        readData(fc);
    }

    // tryLock — non-blocking, returns null if lock cannot be acquired:
    FileLock maybeLock = fc.tryLock();
    if (maybeLock != null) {
        try {
            updateData(fc);
        } finally {
            maybeLock.release();
        }
    } else {
        System.out.println("File is locked by another process");
    }
}

SocketChannel, ServerSocketChannel, DatagramChannel, and Non-Blocking Mode

SocketChannel is the NIO replacement for java.net.Socket for TCP connections. It can operate in blocking mode (default, behaves like Socket) or non-blocking mode (returns 0 from read() if no data is available, returns false from connect() if connection not yet established, and enables registration with a Selector). SocketChannel.open() creates an unconnected channel; open(SocketAddress) opens and connects in one call (blocking). In non-blocking mode, connect() initiates a connection and returns false; the Selector's OP_CONNECT event fires when the connection is established, at which point finishConnect() must be called. ServerSocketChannel is the NIO replacement for java.net.ServerSocket. bind(SocketAddress) binds to a port. accept() returns a new SocketChannel for an incoming connection in blocking mode, or null in non-blocking mode if no connection is pending. In non-blocking mode, OP_ACCEPT fires on the Selector when a connection is waiting. DatagramChannel is the NIO replacement for java.net.DatagramSocket for UDP. receive(ByteBuffer dst) receives a datagram and returns the sender's address; send(ByteBuffer src, SocketAddress target) sends a datagram. In connected mode (connect(SocketAddress)), read() and write() can be used instead, and packets from other senders are silently discarded. Non-blocking mode is set by channel.configureBlocking(false). In non-blocking mode, read() returns 0 if no data is available (not -1 — that signals end-of-stream). This difference is subtle and critical: a return of 0 means "no data yet, try again later", while -1 means "the connection is closed". In blocking mode, 0 is never returned — read() always returns either a positive count or -1. Selector is the multiplexer for non-blocking channels. channel.register(selector, interestOps) registers the channel to be monitored for the specified operations: OP_CONNECT (connection completion), OP_ACCEPT (incoming connection), OP_READ (data available to read), OP_WRITE (space available to write). selector.select() blocks until at least one registered channel is ready; selectNow() is non-blocking. selector.selectedKeys() returns the set of ready SelectionKey objects. Each SelectionKey holds the channel, the selector, and the ready operations. The key must be removed from the selected-keys set after processing — the Selector does not remove it automatically.
Java
// ── SocketChannel blocking mode (simple, same as Socket) ────────────
try (SocketChannel sc = SocketChannel.open(
        new InetSocketAddress("api.example.com", 443))) {
    // Blocking mode by default — read/write behave like Socket streams

    ByteBuffer request = ByteBuffer.wrap(
        "GET / HTTP/1.0
Host: api.example.com

"
        .getBytes(StandardCharsets.US_ASCII));
    sc.write(request);

    ByteBuffer response = ByteBuffer.allocate(8192);
    while (sc.read(response) != -1) {
        response.flip();
        System.out.print(StandardCharsets.US_ASCII.decode(response));
        response.clear();
    }
}

// ── Non-blocking SocketChannel with Selector ─────────────────────────
Selector selector = Selector.open();

// Client connection (non-blocking):
SocketChannel client = SocketChannel.open();
client.configureBlocking(false);
client.connect(new InetSocketAddress("api.example.com", 80));
client.register(selector, SelectionKey.OP_CONNECT);

// Server (non-blocking):
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(8080));
server.register(selector, SelectionKey.OP_ACCEPT);

// Event loop:
while (true) {
    int readyCount = selector.select(1000);   // timeout in ms
    if (readyCount == 0) { handleTimeout(); continue; }

    Set<SelectionKey> selected = selector.selectedKeys();
    for (Iterator<SelectionKey> it = selected.iterator(); it.hasNext(); ) {
        SelectionKey key = it.next();
        it.remove();   // MUST remove — selector won't do this

        try {
            if (key.isConnectable()) {
                SocketChannel ch = (SocketChannel) key.channel();
                if (ch.finishConnect()) {     // complete the connection
                    ch.register(selector, SelectionKey.OP_READ);
                }
            }
            else if (key.isAcceptable()) {
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel incoming = ssc.accept();   // null if spurious wakeup
                if (incoming != null) {
                    incoming.configureBlocking(false);
                    // Attach state to the key for later use:
                    incoming.register(selector, SelectionKey.OP_READ, new ClientState());
                }
            }
            else if (key.isReadable()) {
                SocketChannel ch = (SocketChannel) key.channel();
                ClientState state = (ClientState) key.attachment();
                ByteBuffer buf = ByteBuffer.allocate(4096);
                int n = ch.read(buf);

                if (n == -1) {           // connection closed
                    key.cancel();
                    ch.close();
                } else if (n == 0) {     // no data yet (non-blocking) — safe to ignore
                    // Will get OP_READ again when data arrives
                } else {                 // data available
                    buf.flip();
                    state.append(buf);
                    if (state.isRequestComplete()) {
                        key.interestOps(SelectionKey.OP_WRITE);  // switch to write mode
                        state.prepareResponse();
                    }
                }
            }
            else if (key.isWritable()) {
                SocketChannel ch = (SocketChannel) key.channel();
                ClientState state = (ClientState) key.attachment();
                int written = ch.write(state.responseBuffer());
                if (!state.responseBuffer().hasRemaining()) {
                    key.interestOps(SelectionKey.OP_READ);  // done writing, back to read
                }
            }
        } catch (IOException e) {
            key.cancel();
            key.channel().close();
        }
    }
}

// ── DatagramChannel — UDP ─────────────────────────────────────────────
try (DatagramChannel dc = DatagramChannel.open()) {
    dc.bind(new InetSocketAddress(9999));  // bind to receive

    ByteBuffer buf = ByteBuffer.allocate(65535);
    SocketAddress sender = dc.receive(buf);   // blocks until datagram arrives
    buf.flip();
    System.out.printf("Received %d bytes from %s%n", buf.limit(), sender);

    // Echo back:
    buf.flip();   // rewind to re-read same bytes
    dc.send(buf, sender);
}

// UDP client:
try (DatagramChannel dc = DatagramChannel.open()) {
    ByteBuffer msg = ByteBuffer.wrap("ping".getBytes(StandardCharsets.UTF_8));
    dc.send(msg, new InetSocketAddress("localhost", 9999));
}

Pipe, AsynchronousChannel, and Channel Utilities

Pipe provides a unidirectional channel pair for inter-thread communication within the same JVM. Pipe.open() returns a Pipe with a SinkChannel (writable) and a SourceChannel (readable). Writing to the SinkChannel makes data available on the SourceChannel. Both channels support blocking and non-blocking modes. Pipe is an alternative to PipedInputStream/PipedOutputStream with NIO buffer semantics; it is rarely the first choice for inter-thread communication (BlockingQueue and CompletableFuture are usually better) but is appropriate when the receiver code expects a ReadableByteChannel. AsynchronousFileChannel and AsynchronousSocketChannel provide asynchronous I/O: read and write operations are submitted and return immediately; completion is signaled either via a Future (caller polls or blocks for result) or via a CompletionHandler callback (called on a pool thread when I/O completes). AsynchronousFileChannel is opened with AsynchronousFileChannel.open(Path, OpenOption...) optionally with a custom ExecutorService for the callback thread pool. AsynchronousSocketChannel wraps TCP connections with the same asynchronous model. These channels are backed by OS asynchronous I/O facilities: io_uring on recent Linux, IOCP on Windows, kqueue on macOS. Channels utility class provides conversion methods between channels and streams: Channels.newInputStream(ReadableByteChannel) wraps a channel in an InputStream; Channels.newOutputStream(WritableByteChannel) wraps in OutputStream; Channels.newChannel(InputStream) wraps an InputStream as a ReadableByteChannel; Channels.newReader(ReadableByteChannel, Charset) wraps as a Reader with charset decoding; Channels.newWriter(WritableByteChannel, Charset) wraps as a Writer with charset encoding. These bridges allow legacy stream-based code to consume NIO channels and vice versa. Channel closing semantics differ from stream closing in one important way: closing a channel while another thread is blocked reading or writing that channel causes an AsynchronousCloseException to be thrown in the blocked thread. This allows clean shutdown of blocking channel operations from another thread without interrupt — useful for server shutdown sequences where you want to cleanly terminate blocking accepts.
Java
// ── Pipe — inter-thread channel communication ─────────────────────────
Pipe pipe = Pipe.open();
Pipe.SinkChannel   sink   = pipe.sink();
Pipe.SourceChannel source = pipe.source();

// Producer thread:
Thread producer = new Thread(() -> {
    try {
        ByteBuffer data = ByteBuffer.wrap("Hello via Pipe!".getBytes(StandardCharsets.UTF_8));
        sink.write(data);
        sink.close();   // signals end of stream to source
    } catch (IOException e) { e.printStackTrace(); }
}, "pipe-producer");

// Consumer thread:
Thread consumer = new Thread(() -> {
    try {
        ByteBuffer buf = ByteBuffer.allocate(1024);
        while (source.read(buf) != -1) {
            buf.flip();
            System.out.println(StandardCharsets.UTF_8.decode(buf));
            buf.clear();
        }
    } catch (IOException e) { e.printStackTrace(); }
}, "pipe-consumer");

producer.start(); consumer.start();
producer.join(); consumer.join();

// ── AsynchronousFileChannel — callback-based async I/O ────────────────
Path path = Path.of("large-file.bin");
ExecutorService callbackPool = Executors.newFixedThreadPool(4);

try (AsynchronousFileChannel afc = AsynchronousFileChannel.open(
        path, Set.of(StandardOpenOption.READ), callbackPool)) {

    ByteBuffer buf = ByteBuffer.allocate(65536);

    // Callback style:
    afc.read(buf, 0, null, new CompletionHandler<Integer, Void>() {
        @Override
        public void completed(Integer bytesRead, Void att) {
            buf.flip();
            processBuffer(buf);
            System.out.println("Read " + bytesRead + " bytes on "
                + Thread.currentThread().getName());
        }
        @Override
        public void failed(Throwable exc, Void att) {
            System.err.println("Read failed: " + exc.getMessage());
        }
    });

    // Future style (blocks for result):
    ByteBuffer buf2 = ByteBuffer.allocate(65536);
    Future<Integer> future = afc.read(buf2, 65536);  // read at offset 65536
    int n = future.get(10, TimeUnit.SECONDS);
    System.out.println("Future read: " + n + " bytes");

    // Calling thread is NOT blocked between submit and get():
    doOtherWork();   // runs while I/O is in progress
}

// ── Channels utility — bridge NIO and legacy IO ───────────────────────
// Channel → InputStream:
try (ReadableByteChannel rbc = FileChannel.open(Path.of("data.bin"));
     InputStream is = Channels.newInputStream(rbc)) {
    // Legacy code that needs InputStream:
    legacyParser.parse(is);
}

// InputStream → Channel:
try (InputStream is = new URL("https://example.com/data").openStream();
     ReadableByteChannel src = Channels.newChannel(is);
     FileChannel dest = FileChannel.open(Path.of("download.bin"),
         StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
    dest.transferFrom(src, 0, Long.MAX_VALUE);   // download file
}

// Channel → Reader with charset:
try (FileChannel fc = FileChannel.open(Path.of("text.txt"));
     Reader reader = Channels.newReader(fc, StandardCharsets.UTF_8);
     BufferedReader br = new BufferedReader(reader)) {
    br.lines().forEach(System.out::println);
}

// Channel → Writer with charset:
try (FileChannel fc = FileChannel.open(Path.of("output.txt"),
        StandardOpenOption.CREATE, StandardOpenOption.WRITE);
     Writer writer = Channels.newWriter(fc, StandardCharsets.UTF_8);
     PrintWriter pw = new PrintWriter(writer)) {
    pw.println("Written via NIO channel with charset encoding");
}

// ── Closing a channel interrupts blocked operations ────────────────────
FileChannel fc = FileChannel.open(Path.of("data.bin"));
ByteBuffer buf = ByteBuffer.allocate(1024);

Thread blocker = new Thread(() -> {
    try {
        fc.read(buf);   // may block if data not available (rare for files, common for network)
    } catch (AsynchronousCloseException e) {
        System.out.println("Channel was closed while reading — clean shutdown");
    } catch (IOException e) { e.printStackTrace(); }
});
blocker.start();

Thread.sleep(100);
fc.close();   // interrupts the blocked read in blocker thread → AsynchronousCloseException
blocker.join();

Related Topics in Java NIO

NIO Overview
Java NIO (New I/O), introduced in Java 1.4 as the java.nio package, is a collection of APIs that complement and in many cases supersede the original java.io stream model. NIO introduces three foundational abstractions absent from java.io: buffers (typed, fixed-capacity containers for data that support direct memory allocation outside the Java heap), channels (bidirectional, closeable connections to I/O entities that read and write buffers rather than individual bytes), and selectors (multiplexers that let a single thread monitor multiple channels for readiness, enabling non-blocking I/O at scale). Java 7 extended NIO with the java.nio.file package, known as NIO.2 or JSR-203, which provides the Path interface, the Files utility class, the FileSystem abstraction, WatchService for file system event monitoring, and the AsynchronousFileChannel API. Together, NIO and NIO.2 enable four I/O models unavailable in java.io: non-blocking I/O (channel reads and writes return immediately if data is unavailable), asynchronous I/O (I/O operations are submitted and a callback or Future signals completion), memory-mapped I/O (a file is mapped directly into the JVM's virtual address space for zero-copy access), and scatter/gather I/O (reading into or writing from multiple buffers in a single system call). This entry covers the conceptual architecture of NIO, the relationship between buffers, channels, and selectors, the four I/O models, the NIO.2 additions and how they relate to the core NIO abstractions, the difference between blocking and non-blocking channels, and which NIO API to use for which problem.
Path
Path is the central interface in java.nio.file, introduced in Java 7 as part of NIO.2, representing a location in a file system as a sequence of path elements separated by a delimiter. Unlike java.io.File, Path is an interface backed by a pluggable FileSystem provider — the default provider maps to the OS file system, but ZIP archives, in-memory file systems, and remote file systems can provide their own Path implementations. Path is immutable and does not represent an open file or an existing file system entry; it is purely a path string with structured access and manipulation methods. Path provides substantially better semantics than File: path component access, resolution, relativization, normalization, and iteration over path elements are all first-class operations. Combined with the Files utility class, Path enables all file system operations with proper IOException-throwing semantics. This entry covers all Path construction methods, every path manipulation method in depth, path resolution and relativization with edge cases, normalization and toRealPath(), path iteration and watching, conversion to URI and File, comparison semantics, and cross-platform path handling.
Files Class
java.nio.file.Files is a utility class introduced in Java 7 containing over 70 static methods that perform all file system operations on Path objects: reading and writing file content, querying and setting file metadata, creating and deleting files and directories, copying and moving, directory listing and recursive traversal, checking permissions and existence, managing symbolic links, and obtaining channels and streams. Files is the NIO.2 replacement for the operations previously handled by java.io.File's mutation methods (which return boolean on failure) and for the InputStream/OutputStream factory pattern used with java.io streams. Every Files method throws IOException (or a subclass like NoSuchFileException, FileAlreadyExistsException, DirectoryNotEmptyException, or AccessDeniedException) rather than returning boolean, making error handling explicit and unambiguous. This entry covers every major category of Files method in depth, the complete set of StandardOpenOption and StandardCopyOption values, atomic operations and their filesystem-level semantics, bulk read/write convenience methods, the directory traversal API including Files.walk, Files.find, and Files.walkFileTree, permission and attribute manipulation, and the interplay between Files methods and the underlying OS system calls.
Buffers
A Buffer in Java NIO is a fixed-capacity, typed container for data that serves as the intermediary between application code and channels. Every I/O operation in NIO reads data into a buffer or writes data from a buffer — there is no byte-at-a-time API. The Buffer class hierarchy provides typed buffers for every Java primitive: ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, and DoubleBuffer. ByteBuffer is the foundation type because channels always operate in bytes; the other types are views over a ByteBuffer or are used for in-memory data processing. Every buffer has three state variables — position, limit, and capacity — that control where reads and writes occur and how much data is available. The flip(), clear(), compact(), rewind(), and mark()/reset() operations manipulate these state variables to implement the write-then-read and read-then-write patterns that buffer-based I/O requires. ByteBuffer can be backed by a Java heap array (allocate()) or by off-heap native memory (allocateDirect()), with significant performance implications for I/O operations. This entry covers all buffer state variables and their transitions, every buffer method in depth, heap vs direct buffer performance model, view buffers and byte order, the slice() and duplicate() operations, and the common patterns for buffer-based channel I/O.