January 23, 2026

Java Concurrency in Practice — Chapter 1, but future-proof (with examples + NIO + select/poll)

If Chapter 1 had a vibe, it’s this:

Concurrency is how you cash in on modern hardware and responsive software…
and also how you accidentally summon chaos.

You already captured the main storyline. Below is a “final blog” that includes every point you raised, adds examples, explains simplified async event handling, and connects Java NIO all the way down to Unix select() / poll() (and friends). I’ll also make it future-proof by mapping the chapter’s ideas to modern Java concurrency choices (virtual threads, structured concurrency, etc.).


Table of contents

  1. Why concurrency matters (the “clock-speed wall”)

  2. From bare metal to multitasking OS: utilization, fairness, convenience

  3. Threads: “lightweight processes” + what’s shared vs private

  4. Why threads are used: throughput, responsiveness, simpler modeling

  5. Concurrency is already in your app (Servlets, RMI, Swing, timers, JVM)

  6. The three hazard families (Safety, Liveness, Performance) + examples

  7. Performance hazards (your added point) — why “more threads” can be slower

  8. Simplified handling of async events (tasks, pools, futures, EDT)

  9. Non-blocking I/O: Unix select/poll → Java NIO Selector (with code)

  10. Future-proofing: what changed since JCIP, what still matters

  11. A tiny checklist + exercises for Chapter 2


1) Why concurrency matters: CPUs stopped getting faster “for free”

For years, CPUs got faster mostly by increasing clock speed. Then power/thermal limits made that approach unattractive, and manufacturers shifted toward adding more cores instead.

That’s the key consequence Chapter 1 is pushing:

  • A single-threaded program on a multi-core machine can leave a lot of compute unused.

  • Exploiting concurrency effectively becomes more important as core counts rise.

So concurrency isn’t “an optional advanced topic.” It’s how modern systems scale.


2) From bare metal to multitasking OS: why “multiple things at once” became normal

Early computing often ran one program at a time on bare metal. As systems evolved, operating systems began supporting multiple programs executing simultaneously (or appearing to).

Three motivations you listed are the “holy trinity”:

A) Resource utilization

When a program blocks on I/O (disk/network), the CPU would otherwise sit idle. Let something else run.

B) Fairness

Multiple users/processes should each get a reasonable share of machine resources (no single bully process hogging everything).

C) Convenience

Humans naturally model independent activities. Your tea example nails it:

  • Water boiling = “blocked” time

  • Read news while waiting = overlap waiting with useful work

That’s concurrency as lived experience.


3) Threads: “lightweight processes” and the shared-memory deal

A thread is often called a lightweight process because it has its own execution state, but shares the process memory.

Modern OS schedulers typically treat threads (not processes) as the basic unit of scheduling — so threads get time-sliced and scheduled across cores.

What threads share (inside one process)

  • Heap (objects, shared data structures)

  • Same address space

What threads do not share

  • Stack (local variables per call chain)

  • Program counter, registers (execution context)

Because threads share heap state, without coordination they run asynchronously with respect to each other and can interfere in subtle ways.


4) Why threads are used: the big three benefits

1) Exploiting multi-core hardware

Multiple runnable threads can execute simultaneously on different cores.

2) Better throughput via overlapping I/O waits

While one thread blocks on I/O, another thread can run — improving CPU utilization.

3) Simpler modeling (sometimes!)

A lot of systems become easier to describe as independent activities:

  • “each request is handled independently”

  • “UI responds to events; background work fetches data”

  • “periodic cleanup runs on a timer”

Frameworks often use threads internally so you can write “simple-looking” code.


5) Concurrency is already in your app (even if you didn’t add it)

This is one of the most important Chapter 1 lessons: you might already be concurrent because the platform/framework is.

Examples you mentioned (and yes, they matter)

  • Servlet/JSP containers: multiple requests can call into your code concurrently.

  • Timers / scheduled tasks: time-based tasks run in the background.

  • RMI: remote calls can be served concurrently.

  • Swing/AWT: user interaction is event-driven and asynchronous by nature.

  • JVM housekeeping: background threads exist for runtime behavior.


6) The three hazard families (with concrete examples)

JCIP’s intro gives an extremely useful taxonomy:

A) Safety hazards — “nothing bad happens”

Correctness failures: races, broken invariants, inconsistent state.

Example: non-atomic increment

class Counter {
  private int count = 0;

  public void inc() { count++; }  // NOT atomic
  public int get() { return count; }
}

Two threads can both read the same value and overwrite each other → lost updates.

Example: check-then-act (classic race)

if (!map.containsKey(k)) {
  map.put(k, v);
}

Two threads can both pass the containsKey check and both put.

B) Liveness hazards — “something good eventually happens”

The system doesn’t crash, but it stops making progress.

  • Deadlock: circular waiting

  • Starvation: one thread never gets what it needs (CPU/lock/resource)

  • Livelock: threads actively “react,” but no progress

Deadlock example

final Object A = new Object();
final Object B = new Object();

Thread t1 = new Thread(() -> {
  synchronized (A) {
    synchronized (B) { /* ... */ }
  }
});

Thread t2 = new Thread(() -> {
  synchronized (B) {
    synchronized (A) { /* ... */ }
  }
});

If t1 holds A and t2 holds B, both wait forever.

C) Performance hazards — “it works, but it’s slow”

Concurrency can reduce throughput/latency if overhead dominates (details next section).


7) Performance hazards (your added point) — why threads have real overhead

You were 100% right to add this: threads aren’t free.

Even when multithreading improves throughput in principle, each thread introduces runtime costs:

1) Context switching overhead

When the scheduler suspends one thread and resumes another, it saves/restores execution context. Too many runnable threads → CPU time spent juggling instead of working.

2) Loss of locality (cache pain)

Threads bouncing across cores can trash CPU caches. Even with high CPU usage, throughput can drop because the CPU is constantly reloading data rather than executing logic.

3) Scheduler overhead

More runnable threads means more time deciding who runs next and maintaining scheduling structures.

4) Contention costs

Even with “reasonable” thread counts, shared locks/queues can serialize work:

  • lock contention → waiting

  • wakeups → overhead

  • convoy effects → latency spikes

5) Memory overhead

Each platform thread consumes stack space and runtime bookkeeping. Huge thread counts can burn memory/GC budget.

Performance hazard example: unbounded thread-per-request

while (true) {
  Socket s = server.accept();
  new Thread(() -> handle(s)).start(); // scales... until it doesn't
}

At high load you don’t just get slower — you can get unstable:

  • too many threads

  • too many context switches

  • memory pressure

  • catastrophic tail latency


8) Simplified handling of async events: “don’t model everything as raw threads”

Chapter 1 hints at a better approach: structure concurrency around tasks and events, not around manually created threads everywhere.

A) The “task queue + thread pool” approach (simple, scalable, readable)

Instead of “new Thread per event,” you do:

ExecutorService pool = Executors.newFixedThreadPool(32);

void onEvent(Event e) {
  pool.submit(() -> handleEvent(e));
}

This is “simplified async event handling” in practice:

  • the event arrives (UI click, HTTP request, timer tick)

  • you enqueue work

  • a bounded number of workers execute it

You get concurrency and control.

B) Futures/CompletableFuture for “async flow”

When you want non-blocking composition:

CompletableFuture
  .supplyAsync(this::fetchUser, pool)
  .thenApply(this::enrichUser)
  .thenAccept(this::renderOrRespond);

You model “what happens next” without blocking the caller.

C) Swing: async events + thread confinement

Swing uses event-driven async behavior, but with a strict rule:

  • most UI access must happen on the Event Dispatch Thread (EDT)
    Oracle’s Swing docs: only methods explicitly documented thread-safe are safe off-EDT; all others must run on the EDT. (Oracle Docs)

So you handle long work on a worker thread and post back to EDT:

  • SwingUtilities.invokeLater(...) for UI updates (Oracle Docs)

  • or SwingWorker for the common “background + publish results” pattern (Oracle Docs)


9) Non-blocking I/O: Unix select/poll → Java NIO Selector

This is the “under the hood” part you asked for.

The core problem

If you use blocking I/O like:

read(fd, buf, n);  // blocks until data arrives

…and you have 10,000 connections, then:

  • thread-per-connection becomes expensive

  • or you end up busy-waiting (wasting CPU)

Unix solved this with I/O multiplexing.


A) Unix select() (classic readiness multiplexing)

select() allows a program to monitor multiple file descriptors, waiting until one or more become “ready” for I/O. A descriptor is “ready” if an I/O operation would not block. (man7.org)

Two practical details that matter:

  • select() uses fd_set bitsets, and POSIX limits the size by FD_SETSIZE. (man7.org)

  • On return, the sets are modified in place (so you must reinitialize them each loop). (man7.org)

Tiny pseudo-loop (C-ish):

for (;;) {
  FD_ZERO(&readset);
  FD_SET(server_fd, &readset);
  FD_SET(client_fd, &readset);

  int n = select(maxfd+1, &readset, NULL, NULL, &timeout);
  if (FD_ISSET(server_fd, &readset)) accept_client();
  if (FD_ISSET(client_fd, &readset)) read_client();
}

B) Unix poll() (similar idea, different interface)

poll() performs a similar role to select(): wait for one of a set of file descriptors to become ready for I/O. It takes an array of pollfd structs. (man7.org)

It avoids some select() awkwardness (bitsets + fixed FD range), and is typically preferred over select() on modern systems. (Arch Linux Manual Pages)


C) epoll (Linux) and kqueue (BSD/macOS) — scalable successors

  • epoll monitors many file descriptors for readiness and “scales well to large numbers” of watched descriptors. (man7.org)

  • kqueue provides a general kernel event notification mechanism based on “filters.” (FreeBSD Manual Pages)

These are the usual “industrial” building blocks behind high-concurrency network servers.


D) Java NIO Selector: the Java version of readiness multiplexing

Java NIO gives you:

  • Channel + Buffer

  • Selector to monitor many channels and react when they’re ready

Oracle’s Selector docs describe selection operations (select(), selectNow(), etc.) that identify keys/channels ready for operations. (Oracle Docs)

Minimal NIO skeleton (Java):

Selector selector = Selector.open();

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

while (true) {
  selector.select(); // block until some channel is ready

  for (Iterator<SelectionKey> it = selector.selectedKeys().iterator(); it.hasNext();) {
    SelectionKey key = it.next();
    it.remove();

    if (key.isAcceptable()) {
      SocketChannel client = server.accept();
      client.configureBlocking(false);
      client.register(selector, SelectionKey.OP_READ);
    }

    if (key.isReadable()) {
      SocketChannel client = (SocketChannel) key.channel();
      ByteBuffer buf = ByteBuffer.allocate(4096);
      int n = client.read(buf);
      if (n < 0) client.close();
      // process buf...
    }
  }
}

How does Java map this to the OS?
The JDK uses platform-specific selector providers internally. For example, OpenJDK has an EPollSelectorProvider for Linux. (GitHub)
And it has a KQueueSelectorProvider for macOS. (GitHub)

So conceptually:

  • Unix gives you select/poll/epoll/kqueue

  • Java gives you Selector

  • the JDK bridges them with a platform-appropriate provider

That’s the missing “why NIO exists” piece from Chapter 1:

  • thread-per-client hit limits

  • multiplexed readiness APIs evolved

  • NIO exposes that model to Java


10) RMI and concurrency: yes, same remote method can be called concurrently

You asked:

Can the same remote method on the same remote object be called simultaneously by multiple RMI threads?

Yes, it may. Oracle’s RMI architecture spec states:

  • RMI makes no guarantees about mapping invocations to threads.

  • Remote invocation on the same remote object may execute concurrently, so the remote object implementation must be thread-safe. (Oracle Docs)

So treat remote objects like servlets:

  • assume concurrent calls

  • guard shared mutable state

  • design for thread safety


11) Future-proofing this Chapter 1 mindset (2026+)

JCIP is older, but Chapter 1 is still structurally correct. What’s changed is the set of tools available.

A) Choose the model based on the workload

If mostly CPU-bound (pure computation):

  • limit concurrency to core count-ish

  • avoid contention

  • consider fork/join or parallel streams for data-parallel work

If mostly I/O-bound (network, DB, remote calls):

  • concurrency is about overlapping waits

  • you can scale with:

B) Virtual threads (modern “thread-per-task” without the old pain)

Virtual threads are designed to dramatically reduce the cost of high-concurrency applications and were finalized in Java 21 (JEP 444). (openjdk.org)

This matters because it makes the old “thread-per-request” style viable again for many I/O-heavy servers—without thousands of heavyweight platform threads.

C) Structured concurrency (making async lifecycles less error-prone)

Structured concurrency is an approach to preserve parent/child relationships between tasks to improve readability, maintainability, cancellation, and observability. (openjdk.org)
As of JDK 25 it’s still in preview iterations (JEP 505). (openjdk.org)

D) Stick to LTS for production baselines

Oracle’s roadmap lists Java 21 and Java 25 as LTS releases (among others), and indicates the LTS cadence. (Oracle)

Future-proof principle:
Even if APIs evolve, the Chapter 1 hazard categories don’t. Safety, liveness, performance remain the real game.


12) Mini checklist for Chapter 2 (Thread Safety) + exercises

Thread-safety checklist (quick mental scan)

  1. What state is shared? (heap fields, statics, caches, singletons)

  2. Is that state mutable? (if yes, danger)

  3. Are there compound actions? (check-then-act, read-modify-write)

  4. What’s the coordination strategy?

    • lock? atomic? immutable? confinement?

  5. What’s the liveness risk?

    • lock ordering? blocking calls? bounded pools?

  6. What’s the performance plan?

    • thread count? contention? queue depth? tail latency?

Small exercises (fast learning, big payoff)

  • Create a shared count++ bug with two threads; fix with AtomicInteger.

  • Create the two-lock deadlock; fix with consistent lock ordering.

  • Implement a tiny selector-based echo server (NIO) and compare it conceptually to thread-per-connection.

  • In Swing: freeze UI by doing work on EDT, then fix using SwingWorker. (Oracle Docs)


Closing: what Chapter 1 really arms you with

By the end of Chapter 1, you’ve built the exact mental model you need:

  • Concurrency exists because hardware + I/O realities demand it.

  • Threads help utilization, responsiveness, and modeling.

  • Threads also create safety, liveness, and performance hazards.

  • OS I/O multiplexing (select/poll etc.) is the foundation of non-blocking/event-driven servers, and Java NIO is the Java face of that world.

  • Modern Java adds new tools (virtual threads, structured concurrency), but the core reasoning remains exactly what JCIP teaches.

If you want, for Chapter 2, paste any code snippet you’re reading (even a short one) and I’ll do a “thread-safety review” on it using the checklist above—like a mini code review, but focused on atomicity/visibility/invariants.