Java Platform · Technical Deep Dive

Java I/O, NIO &
Network Programming

A comprehensive technical reference covering the full Java I/O stack — from classic byte streams to non-blocking channels, memory-mapped files, TCP/UDP sockets, the modern HttpClient API, SSL/TLS, Asynchronous I/O, and Virtual Thread integration.

Java 21+ java.io java.nio java.net java.net.http SSL/TLS Virtual Threads
01

Classic I/O — java.io

The java.io package, introduced in Java 1.0, provides blocking, stream-oriented I/O. Every operation blocks the calling thread until data is available or written. Despite its age, it remains widely used for simplicity and compatibility.

Stream Hierarchy

All I/O in java.io descends from four abstract roots:

java.io ┌─────────────────────────────────────────────────────┐ │ InputStream OutputStream │ │ └─ FileInputStream └─ FileOutputStream │ │ └─ FilterInputStream └─ FilterOutputStream │ │ └─ BufferedInputStream └─ BufferedOutputStream │ │ └─ DataInputStream └─ DataOutputStream │ │ └─ PushbackInputStream └─ PrintStream │ │ └─ ByteArrayInputStream └─ ByteArrayOutputStream │ │ └─ ObjectInputStream └─ ObjectOutputStream │ │ └─ PipedInputStream └─ PipedOutputStream │ │ │ │ Reader Writer │ │ └─ InputStreamReader └─ OutputStreamWriter │ │ └─ FileReader └─ FileWriter │ │ └─ BufferedReader └─ BufferedWriter │ │ └─ StringReader └─ StringWriter │ │ └─ CharArrayReader └─ CharArrayWriter │ │ └─ PipedReader └─ PipedWriter │ └─────────────────────────────────────────────────────┘
📌 Key Design: Decorator Pattern
java.io uses the Decorator (Wrapper) pattern. You wrap a raw stream with filtering streams to add capabilities (buffering, data conversion, compression) without modifying the underlying stream.

Byte Streams vs. Character Streams

AspectInputStream / OutputStreamReader / Writer
Unitbyte (8-bit)char (16-bit Unicode)
EncodingNone — raw bytesUses Charset (UTF-8, ISO-8859-1…)
BridgeInputStreamReader / OutputStreamWriter
Use caseBinary data, images, serializationText files, XML, JSON
// Always specify charset explicitly — never rely on platform default
try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(
            new FileInputStream("data.txt"),
            StandardCharsets.UTF_8))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} // AutoCloseable — stream closed automatically
Java

Buffered I/O

Unbuffered reads/writes invoke a system call per byte — catastrophic for performance. BufferedInputStream and BufferedReader maintain an in-memory buffer (default 8 KB) and fill it in bulk.

// Benchmark: Unbuffered vs Buffered file copy
// Unbuffered: ~50ms for 10MB   Buffered: ~3ms  (~16x faster)

public static void bufferedCopy(Path src, Path dst) throws IOException {
    try (var in  = new BufferedInputStream(new FileInputStream(src.toFile()), 65536);
         var out = new BufferedOutputStream(new FileOutputStream(dst.toFile()), 65536)) {
        byte[] buf = new byte[65536];
        int n;
        while ((n = in.read(buf)) != -1) {
            out.write(buf, 0, n);
        }
    }
}
Java
⚠️ Flush Before Close
BufferedOutputStream does not auto-flush on every write. Call flush() or close the stream (which calls flush internally) to ensure all data is written to disk. Failure to flush can silently lose data.

Object Serialization

// Serializing an object graph to bytes
record Point(double x, double y) implements Serializable {
    @Serial private static final long serialVersionUID = 1L;
}

try (var oos = new ObjectOutputStream(
                  new BufferedOutputStream(
                      new FileOutputStream("point.ser")))) {
    oos.writeObject(new Point(3.0, 4.0));
}

// Deserializing — restrict classes to prevent gadget attacks
try (var ois = new ObjectInputStream(
                  new BufferedInputStream(
                      new FileInputStream("point.ser"))) {
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc)
            throws ClassNotFoundException, IOException {
        if (!desc.getName().equals(Point.class.getName()))
            throw new InvalidClassException("Unauthorized: ", desc.getName());
        return super.resolveClass(desc);
    }
}) {
    Point p = (Point) ois.readObject();
}
Java
🔴 Security Warning
Java Object Serialization is a notorious attack surface (CVE-2015-4852 and many others). Prefer JSON (Jackson/Gson), Protocol Buffers, or Records with explicit parsing. If you must use it, use ObjectInputFilter (JEP 290) to allowlist deserializable types.
02

NIO — java.nio

Introduced in Java 1.4 (NIO) and extended in Java 7 (NIO.2), the java.nio package provides buffer-oriented, channel-based, optionally non-blocking I/O. The core abstraction shift: data moves to/from Buffers through Channels, enabling zero-copy transfers and selector-based multiplexing.

📦

Buffers

Fixed-capacity containers for primitive data. The unit of all NIO data transfer.

🔌

Channels

Bidirectional conduits to I/O resources (files, sockets). Support scatter/gather.

🎛️

Selectors

Multiplex many channels on a single thread. Foundation of async servers.

📁

NIO.2 Path API

Modern file system API. Replaces legacy java.io.File.

Buffers — Deep Dive

A Buffer is a fixed-capacity array plus three bookmarks: position, limit, and capacity.

ByteBuffer internals after allocate(8): ┌────┬────┬────┬────┬────┬────┬────┬────┐ │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ └────┴────┴────┴────┴────┴────┴────┴────┘ ↑ ↑ position=0 limit=capacity=8 After put("ABC"): ┌────┬────┬────┬────┬────┬────┬────┬────┐ │ 65 │ 66 │ 67 │ 0 │ 0 │ 0 │ 0 │ 0 │ └────┴────┴────┴────┴────┴────┴────┴────┘ ↑ ↑ position=3 limit=capacity=8 After flip(): (prepare for reading) ┌────┬────┬────┬────┬────┬────┬────┬────┐ │ 65 │ 66 │ 67 │ 0 │ 0 │ 0 │ 0 │ 0 │ └────┴────┴────┴────┴────┴────┴────┴────┘ ↑ ↑ position=0 limit=3 (capacity=8 unchanged)
// Buffer lifecycle: allocate → write → flip → read → clear/compact
ByteBuffer buf = ByteBuffer.allocate(1024);       // heap buffer
ByteBuffer dir = ByteBuffer.allocateDirect(1024); // direct (off-heap) buffer

// Writing data into buffer
buf.put((byte) 65);          // absolute
buf.putInt(42);              // typed put
buf.put("hello".getBytes()); // bulk

// Switch to read mode
buf.flip();                   // limit=position, position=0

while (buf.hasRemaining()) {
    byte b = buf.get();        // reads one byte, advances position
}

// Partial reads: compact() shifts unread data to beginning
buf.compact();   // unread bytes → [0..n], position=n, limit=capacity
buf.clear();    // full reset: position=0, limit=capacity (data untouched!)
Java

Heap vs. Direct Buffers

PropertyHeap BufferDirect Buffer
Memory locationJVM heapOS native memory (off-heap)
GC managedYesNo (Cleaner / PhantomReference)
Allocation costLowHigh
I/O throughputLower (extra copy)Higher (zero-copy path)
Best useShort-lived, small dataLong-lived, large I/O buffers

Channels

Channels replace streams for NIO I/O. Unlike streams they are bidirectional, support scatter/gather I/O, and can be placed in non-blocking mode.

// FileChannel — zero-copy transfer between channels
try (var in  = FileChannel.open(src, StandardOpenOption.READ);
     var out = FileChannel.open(dst,
                   StandardOpenOption.WRITE,
                   StandardOpenOption.CREATE,
                   StandardOpenOption.TRUNCATE_EXISTING)) {

    long transferred = 0, size = in.size();
    while (transferred < size) {
        transferred += in.transferTo(transferred, size - transferred, out);
        // transferTo uses sendfile(2) on Linux — true zero-copy!
    }
}

// Scatter read — fill multiple buffers in one syscall
ByteBuffer header = ByteBuffer.allocate(12);
ByteBuffer body   = ByteBuffer.allocate(4096);
channel.read(new ByteBuffer[]{ header, body }); // scatter

// Gather write — drain multiple buffers in one syscall
channel.write(new ByteBuffer[]{ header, body }); // gather
Java

Selectors — Event-Driven Multiplexing

A Selector monitors multiple SelectableChannels and blocks until at least one is ready for I/O. This enables a single thread to manage thousands of connections — the core of Reactor-pattern servers.

Thread │ └─► Selector.select() ◄─── blocks until event ───────────────┐ │ │ ▼ │ selectedKeys() │ │ │ ┌────┴──────────────────────────────────────────────┐ │ │OP_ACCEPT OP_CONNECT OP_READ OP_WRITE │ │ └──────────────────────────────────────────────────┘ │ │ │ │ │ │ ServerSC SocketCH SocketCH SocketCH ─────────────────┘ (listen) (client1) (client2) (client3) ↑ register(selector, OP_READ)
// Non-blocking server skeleton using Selector
Selector selector = Selector.open();

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select();  // blocks until ≥1 channel ready

    var keys = selector.selectedKeys().iterator();
    while (keys.hasNext()) {
        SelectionKey key = keys.next();
        keys.remove(); // MUST remove manually

        if (key.isAcceptable()) {
            SocketChannel client = ssc.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ,
                           new ByteBuffer[]{ByteBuffer.allocate(8192)});

        } else if (key.isReadable()) {
            SocketChannel ch = (SocketChannel) key.channel();
            ByteBuffer[]  bufs = (ByteBuffer[]) key.attachment();
            long n = ch.read(bufs);
            if (n == -1) { ch.close(); continue; }
            processRequest(ch, bufs[0]);
        }
    }
}
Java
💡 Selector Keys & Attachment
Each registered channel gets a SelectionKey. Use key.attach(obj) to associate per-connection state (buffers, protocol state machines) with its key — no need for external Maps.

NIO.2 — Modern File API

java.nio.file.Path, Files, and FileSystem (Java 7) replace the error-prone java.io.File. All methods throw proper checked exceptions instead of silently returning false.

Path p = Path.of("/var/data", "report.csv");

// Atomic read / write
String text  = Files.readString(p, StandardCharsets.UTF_8);
List<String> lines = Files.readAllLines(p, StandardCharsets.UTF_8);
Files.writeString(p, "new content", StandardOpenOption.APPEND);

// Stream-based directory walk (lazy, no memory explosion)
try (Stream<Path> walk = Files.walk(p.getParent(), 3)) {
    walk.filter(Files::isRegularFile)
        .filter(f -> f.toString().endsWith(".csv"))
        .forEach(System.out::println);
}

// WatchService — filesystem events
WatchService watcher = FileSystems.getDefault().newWatchService();
p.getParent().register(watcher,
    StandardWatchEventKinds.ENTRY_CREATE,
    StandardWatchEventKinds.ENTRY_MODIFY,
    StandardWatchEventKinds.ENTRY_DELETE);

WatchKey key = watcher.take(); // blocks
for (WatchEvent<?> event : key.pollEvents()) {
    System.out.printf("%s: %s%n", event.kind(), event.context());
}
key.reset(); // MUST reset to receive future events
Java

Memory-Mapped Files

Memory-mapped files expose a file as a ByteBuffer backed by OS virtual memory pages. The OS handles paging — no explicit read/write syscalls needed. Ideal for random access to large files.

try (var fc = FileChannel.open(path, StandardOpenOption.READ)) {
    MappedByteBuffer mmap = fc.map(
        FileChannel.MapMode.READ_ONLY,
        0,          // offset
        fc.size()   // length
    );
    // Access bytes directly — OS pages in on demand
    while (mmap.hasRemaining()) {
        process(mmap.get());
    }
    // Force dirty pages to storage
    mmap.force();
}
// Note: MappedByteBuffer is NOT automatically unmapped on GC.
// Use sun.misc.Cleaner (or MethodHandles on Java 9+) for explicit unmap.
Java
03

TCP Sockets

TCP provides reliable, ordered, stream-oriented communication. Java exposes it via java.net.Socket (client) and java.net.ServerSocket (server).

// ─── TCP Server ────────────────────────────────────────────────
try (var serverSocket = new ServerSocket()) {
    serverSocket.setReuseAddress(true);            // SO_REUSEADDR
    serverSocket.setReceiveBufferSize(65536);
    serverSocket.bind(new InetSocketAddress(8080), 128); // backlog=128

    ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor(); // Java 21

    while (!serverSocket.isClosed()) {
        Socket client = serverSocket.accept();   // blocks
        pool.submit(() -> handleClient(client));
    }
}

static void handleClient(Socket socket) {
    try (socket;
         var in  = new BufferedInputStream(socket.getInputStream());
         var out = new BufferedOutputStream(socket.getOutputStream())) {

        socket.setSoTimeout(30_000);     // 30 s read timeout
        socket.setTcpNoDelay(true);      // disable Nagle for latency
        socket.setKeepAlive(true);       // detect dead peers

        byte[] buf = new byte[8192];
        int n;
        while ((n = in.read(buf)) != -1) {
            out.write(buf, 0, n); // echo
            out.flush();
        }
    } catch (SocketTimeoutException e) {
        System.err.println("Client timed out");
    } catch (IOException e) {
        System.err.println("Client error: " + e.getMessage());
    }
}
Java

Important Socket Options

OptionDefaultPurpose
SO_REUSEADDRfalseReuse TIME_WAIT addresses — essential for servers
SO_KEEPALIVEfalseSend TCP keepalives to detect dead peers
TCP_NODELAYfalseDisable Nagle's algorithm — reduces latency for small writes
SO_RCVBUF / SO_SNDBUFOS defaultSocket buffer size — tune for high-bandwidth links
SO_TIMEOUT0 (infinite)Read timeout in milliseconds
SO_LINGERdisabledBlock on close until data sent or timeout
04

UDP Datagrams

UDP is connectionless, unreliable, unordered — but with lower overhead. Use for DNS lookups, real-time media (VoIP, games), and QUIC-based protocols.

// UDP Server
try (var socket = new DatagramSocket(9090)) {
    byte[] buf = new byte[65507]; // max UDP payload
    DatagramPacket pkt = new DatagramPacket(buf, buf.length);

    while (true) {
        socket.receive(pkt);   // blocks until datagram arrives
        String msg = new String(pkt.getData(), 0, pkt.getLength(),
                                  StandardCharsets.UTF_8);
        // Echo back to sender
        byte[] reply = ("Echo: " + msg).getBytes();
        socket.send(new DatagramPacket(reply, reply.length,
                          pkt.getAddress(), pkt.getPort()));
    }
}

// Multicast group join (NIO DatagramChannel)
DatagramChannel mc = DatagramChannel.open(StandardProtocolFamily.INET);
mc.setOption(StandardSocketOptions.SO_REUSEADDR, true);
mc.bind(new InetSocketAddress(5353));
NetworkInterface ni = NetworkInterface.getByName("eth0");
mc.join(InetAddress.getByName("224.0.0.251"), ni); // mDNS group
Java
05

High-Performance Non-Blocking Server

Combining ServerSocketChannel, Selector, and a Reactor pattern yields a server that handles thousands of concurrent connections with a small, fixed thread pool.

// Production-grade Reactor — single-threaded event loop with worker pool
public class ReactorServer {

    private final Selector          selector;
    private final ServerSocketChannel ssc;
    private final ExecutorService    workers =
        Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public ReactorServer(int port) throws IOException {
        selector = Selector.open();
        ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);
        ssc.setOption(StandardSocketOptions.SO_REUSEADDR, true);
        ssc.bind(new InetSocketAddress(port), 1024);
        ssc.register(selector, SelectionKey.OP_ACCEPT);
    }

    public void run() throws IOException {
        while (!Thread.currentThread().isInterrupted()) {
            if (selector.select(1000) == 0) continue; // timeout, check shutdown
            var it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey k = it.next(); it.remove();
                try {
                    if      (k.isAcceptable()) onAccept(k);
                    else if (k.isReadable())  onRead(k);
                    else if (k.isWritable()) onWrite(k);
                } catch (IOException e) { closeKey(k); }
            }
        }
    }

    private void onAccept(SelectionKey k) throws IOException {
        SocketChannel ch = ((ServerSocketChannel) k.channel()).accept();
        if (ch == null) return;
        ch.configureBlocking(false);
        ch.setOption(StandardSocketOptions.TCP_NODELAY, true);
        ch.register(selector, SelectionKey.OP_READ, new ConnectionCtx());
    }

    private void onRead(SelectionKey k) throws IOException {
        SocketChannel ch  = (SocketChannel) k.channel();
        ConnectionCtx ctx = (ConnectionCtx) k.attachment();
        int n = ch.read(ctx.readBuf);
        if (n == -1) { closeKey(k); return; }
        if (ctx.isRequestComplete()) {
            var snapshot = ctx.drainRequest();
            workers.submit(() -> {
                ByteBuffer resp = processRequest(snapshot);
                ctx.enqueueResponse(resp);
                k.interestOpsOr(SelectionKey.OP_WRITE);
                selector.wakeup(); // wake selector from worker thread
            });
        }
    }
}
Java
06

HttpClient API (Java 11+)

java.net.http.HttpClient is the modern, built-in HTTP client supporting HTTP/1.1, HTTP/2, WebSocket, synchronous and asynchronous modes, and reactive streaming via Flow.

// Build once, reuse for all requests (thread-safe)
HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)
    .connectTimeout(Duration.ofSeconds(5))
    .followRedirects(HttpClient.Redirect.NORMAL)
    .executor(Executors.newVirtualThreadPerTaskExecutor())
    .build();

// ── Synchronous GET ──────────────────────────────────────────
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/items"))
    .header("Accept", "application/json")
    .timeout(Duration.ofSeconds(10))
    .GET()
    .build();

HttpResponse<String> resp =
    client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.statusCode() + " " + resp.body());

// ── Async POST with JSON body ─────────────────────────────────
CompletableFuture<HttpResponse<String>> future = client.sendAsync(
    HttpRequest.newBuilder()
        .uri(URI.create("https://api.example.com/items"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"widget\"}"))
        .build(),
    HttpResponse.BodyHandlers.ofString()
).thenApply(r -> {
    if (r.statusCode() != 201)
        throw new RuntimeException("Unexpected status: " + r.statusCode());
    return r;
});

// ── Streaming response body via BodyHandler.ofInputStream ────
HttpResponse<InputStream> streamResp =
    client.send(req, HttpResponse.BodyHandlers.ofInputStream());
try (var is = streamResp.body()) {
    // process in chunks — no full body buffering in memory
    is.transferTo(System.out);
}
Java

WebSocket

WebSocket ws = client.newWebSocketBuilder()
    .header("Authorization", "Bearer token")
    .buildAsync(URI.create("wss://echo.example.com"), new WebSocket.Listener() {
        private final StringBuilder sb = new StringBuilder();

        @Override
        public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
            sb.append(data);
            if (last) {
                System.out.println("Received: " + sb);
                sb.setLength(0);
            }
            ws.request(1); // flow control
            return null;
        }
    }).join();

ws.sendText("Hello WebSocket", true).join();
ws.sendClose(WebSocket.NORMAL_CLOSURE, "done");
Java
07

SSL / TLS

Java's javax.net.ssl package wraps Socket and SocketChannel with TLS. The core components are SSLContext, SSLEngine, and SSLSocket.

// ── Mutual TLS (mTLS) Server ──────────────────────────────────
// 1. Load server keystore (certificate + private key)
KeyStore ks = KeyStore.getInstance("PKCS12");
try (var fis = new FileInputStream("server.p12")) {
    ks.load(fis, "changeit".toCharArray());
}

KeyManagerFactory kmf = KeyManagerFactory.getInstance(
    KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, "changeit".toCharArray());

// 2. Load truststore (trusted CA certs)
KeyStore ts = KeyStore.getInstance("JKS");
try (var fis = new FileInputStream("truststore.jks")) {
    ts.load(fis, "trustpass".toCharArray());
}

TrustManagerFactory tmf = TrustManagerFactory.getInstance(
    TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);

// 3. Build SSLContext with TLS 1.3
SSLContext ctx = SSLContext.getInstance("TLSv1.3");
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

SSLServerSocketFactory ssf = ctx.getServerSocketFactory();
try (var sss = (SSLServerSocket) ssf.createServerSocket(8443)) {
    sss.setNeedClientAuth(true); // require client certificate (mTLS)
    sss.setEnabledProtocols(new String[]{ "TLSv1.3" });

    while (true) {
        SSLSocket client = (SSLSocket) sss.accept();
        // Kick off handshake on virtual thread
        Thread.startVirtualThread(() -> handleTlsClient(client));
    }
}
Java

SSLEngine — Non-Blocking TLS

SSLEngine decouples TLS handshaking from transport, enabling use with NIO channels.

SSLEngine engine = ctx.createSSLEngine("peer.example.com", 443);
engine.setUseClientMode(true);
engine.setEnabledProtocols(new String[]{ "TLSv1.3" });
engine.setEnabledCipherSuites(new String[]{ "TLS_AES_256_GCM_SHA384" });
engine.beginHandshake();

// Handshake loop — pump wrap/unwrap until FINISHED
while (engine.getHandshakeStatus() != SSLEngineResult.HandshakeStatus.FINISHED) {
    switch (engine.getHandshakeStatus()) {
        case NEED_WRAP   -> doWrap(engine, channel, netOutBuf);
        case NEED_UNWRAP -> doUnwrap(engine, channel, netInBuf, appInBuf);
        case NEED_TASK   -> engine.getDelegatedTask().run(); // offload to thread pool
        default          -> throw new IllegalStateException();
    }
}
Java
08

Asynchronous I/O (AIO)

Introduced in Java 7, AsynchronousFileChannel and AsynchronousSocketChannel provide true kernel-level async I/O (IOCP on Windows, epoll/io_uring on Linux) via CompletionHandlers or Futures.

// Async file read with CompletionHandler
try (var afc = AsynchronousFileChannel.open(
        path, StandardOpenOption.READ)) {

    ByteBuffer buf = ByteBuffer.allocate((int) afc.size());

    afc.read(buf, 0, buf, new CompletionHandler<Integer, ByteBuffer>() {
        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            attachment.flip();
            System.out.println("Read " + result + " bytes");
        }
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            exc.printStackTrace();
        }
    });
    Thread.sleep(Duration.ofSeconds(1)); // keep JVM alive
}

// Async TCP client with Future-based API
AsynchronousSocketChannel asc = AsynchronousSocketChannel.open();
Future<Void> connected = asc.connect(new InetSocketAddress("host", 8080));
connected.get(); // block until connected

ByteBuffer data = ByteBuffer.wrap("GET / HTTP/1.1\r\n\r\n".getBytes());
Future<Integer> written = asc.write(data);
System.out.println("Wrote: " + written.get() + " bytes");
Java
09

Virtual Threads & I/O (Java 21)

Project Loom's Virtual Threads (JEP 444, Java 21) are cheap, JVM-managed threads that automatically unmount from the carrier thread when blocked on I/O. This eliminates the reactive programming complexity of Selector-based code while achieving comparable throughput.

Platform Threads: Virtual Threads (Loom): ┌───────────┐ ┌───────────┐ │ Thread-1 │ blocked on │ VThread-1 │ → mounted on OS thread │ │ socket read │ │ │ (stalled) │ └───────────┘ ← I/O blocks? unmount! └───────────┘ ┌───────────┐ ┌───────────┐ │ VThread-2 │ → mounted instead │ Thread-2 │ └───────────┘ │ (stalled) │ ┌───────────┐ └───────────┘ │ VThread-3 │ → mounted ... └───────────┘ N threads, N blocked 1 OS thread, N concurrent VTs!
// Virtual thread per connection — dead simple, high concurrency
try (var server = new ServerSocket(8080)) {
    try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
        while (true) {
            Socket s = server.accept(); // blocking — fine on VT!
            exec.submit(() -> {
                try (s; var in = s.getInputStream()) {
                    // blocking I/O here — VT unmounts, doesn't waste OS thread
                    byte[] data = in.readAllBytes();
                    process(data);
                }
            });
        }
    }
}

// Virtual threads + HttpClient: maximum simplicity + throughput
HttpClient client = HttpClient.newBuilder()
    .executor(Executors.newVirtualThreadPerTaskExecutor())
    .build();

// Structured Concurrency (JEP 453) — fan out to 100 endpoints
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    List<StructuredTaskScope.Subtask<String>> tasks = urls.stream()
        .map(url -> scope.fork(() -> fetchUrl(client, url)))
        .toList();
    scope.join().throwIfFailed();
    List<String> results = tasks.stream().map(StructuredTaskScope.Subtask::get).toList();
}
Java
⚡ Virtual Thread I/O Rules
  • All java.io and java.net blocking operations automatically support unmounting on VTs.
  • Avoid synchronized blocks that contain I/O — they pin the VT to its carrier thread. Use ReentrantLock instead (Java 21 unfixes this for most cases automatically).
  • ThreadLocal in VTs works but is expensive at scale — prefer ScopedValue (JEP 446).
10

Performance Patterns

Buffer Sizing Strategy

ScenarioRecommended SizeRationale
Network socket reads8 – 64 KBMatches typical MTU multiples
File copy64 – 256 KBAmortizes syscall overhead
Memory-mapped accessFull file or page-aligned chunksOS paging efficiency
SSL/TLS records≥ 16 KB + overheadMax TLS record = 16384 bytes

Object Pooling for Buffers

// Avoid DirectByteBuffer allocation churn with a pool
public class BufferPool {
    private final ArrayDeque<ByteBuffer> pool = new ArrayDeque<>();
    private final int capacity;

    public BufferPool(int capacity) { this.capacity = capacity; }

    public synchronized ByteBuffer acquire() {
        ByteBuffer buf = pool.poll();
        return buf != null ? buf.clear() : ByteBuffer.allocateDirect(capacity);
    }

    public synchronized void release(ByteBuffer buf) {
        if (pool.size() < 64) pool.push(buf);
        // else discard — let GC reclaim
    }
}
Java

Zero-Copy Transfer

// Serve a file over socket without copying to user-space
try (var fileChannel   = FileChannel.open(filePath, StandardOpenOption.READ);
     var socketChannel = SocketChannel.open(serverAddr)) {

    long size        = fileChannel.size();
    long transferred = 0;
    while (transferred < size) {
        long n = fileChannel.transferTo(transferred, size - transferred, socketChannel);
        if (n <= 0) break; // connection closed
        transferred += n;
    }
    // Linux: sendfile(2) — data never enters user-space heap
    // macOS: sendfile(2) with header/trailer support
    // Windows: TransmitFile
}
Java
11

Common Pitfalls & Best Practices

🔴 Forgetting to remove SelectionKey
After processing selector.selectedKeys(), you must call iterator.remove(). The selector never removes keys itself. Failure causes infinite re-processing of the same ready key.
🔴 Partial Reads / Writes
channel.read(buf) may read fewer bytes than buf.remaining(). channel.write(buf) may write fewer bytes than the buffer contains. Always loop until the buffer is drained or the channel signals end-of-stream.
⚠️ Platform-Default Charset
new InputStreamReader(is) and new String(bytes) use the platform default charset (varies by OS). Always specify StandardCharsets.UTF_8 explicitly.
⚠️ Not Closing Resources
Every stream, channel, and socket is an OS resource. Use try-with-resources religiously. Leaked file descriptors cause Too many open files errors under load.
⚠️ Blocking I/O Inside synchronized on Virtual Threads
synchronized blocks pin virtual threads to carrier threads. A pinned VT doing blocking I/O defeats the purpose of virtual threads. Replace with java.util.concurrent.locks.ReentrantLock.
✅ Best Practice: Connection Timeout vs Read Timeout
Always set both: connect timeout (socket.connect(addr, timeout)) and read timeout (socket.setSoTimeout(ms)). A missing read timeout causes a thread to block permanently on a stalled peer — a classic cause of thread pool exhaustion.

Quick Reference Cheat Sheet

TaskBest API (Java 21)
Read a small text fileFiles.readString(path, UTF_8)
Stream a large file line-by-lineFiles.lines(path, UTF_8) (try-with-resources)
Copy a file efficientlyFiles.copy(src, dst, REPLACE_EXISTING)
Watch a directory for changesWatchService via FileSystems.getDefault()
Simple HTTP GETHttpClient.send(req, BodyHandlers.ofString())
High-concurrency TCP serverServerSocket + newVirtualThreadPerTaskExecutor()
100k+ concurrent connectionsNIO Selector + SocketChannel reactor
Zero-copy file servingFileChannel.transferTo()
Secure socketSSLContextSSLSocket / SSLEngine
Non-blocking TLSSSLEngine + SocketChannel
Covers Java SE 21 · Generated with care · All code is production-intent, not toy examples.