January 23, 2026

Java Concurrency in Practice (JCIP) — Chapter 1

Why Concurrency Exists, Why It’s Hard, and How to Talk About It (with diagrams + examples for your YouTube video)

Hook: Concurrency is basically “reading the news while the kettle boils.”
The tricky part is: if two people try to make tea using the same kitchen counter at the same time… stuff gets knocked over.

This blog is a complete, future-proof Chapter 1 recap (plus the missing pieces you asked for: async event handling, non-blocking I/O, Unix select()/poll(), and modern Java updates). It’s designed so you can lift sections directly into a YouTube script and use the diagrams as visuals.


1) The reason concurrency became mandatory: the clock-speed wall → multi-core world

CPU performance used to rise mainly by increasing clock speed. That path got limited by power/heat constraints, so manufacturers shifted to adding more cores.

Consequence: A single-threaded program often can’t use all available CPU capacity on a multi-core machine. If your machine has 8 cores and you run 1 runnable thread, you’re leaving a lot on the table.

Diagram: “One thread on many cores”

4-core CPU
+-------+-------+-------+-------+
| Core1 | Core2 | Core3 | Core4 |
|  RUN  | idle  | idle  | idle  |
+-------+-------+-------+-------+

4 runnable threads
+-------+-------+-------+-------+
| Core1 | Core2 | Core3 | Core4 |
| RUN   | RUN   | RUN   | RUN   |
+-------+-------+-------+-------+

2) A short history: from bare metal to multitasking OS

Early systems often ran one program at a time. Then operating systems evolved to run multiple programs “at once” (time-slicing, scheduling) because of:

A) Resource utilization

If one program blocks on I/O (disk/network), the CPU shouldn’t sit idle—run something else.

B) Fairness

Multiple users/processes should get a fair share of resources.

C) Convenience

Humans naturally model the world as independent activities.
Your tea example is perfect:

  • Boiling water = waiting (blocked)

  • Read news while waiting = overlap idle time with useful work

That’s concurrency as real life.


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

Modern OSs schedule threads as the basic unit (in practice, most systems treat threads—not processes—as what gets scheduled).

What threads share (inside one process)

What each thread owns

  • Stack

  • Program counter / registers (execution context)

Diagram: process vs threads

flowchart LR
  subgraph P["One Process (one address space)"]
    Heap[(Heap / Objects / Shared State)]
    subgraph T1["Thread 1"]
      S1[Stack]
      C1[PC + Registers]
    end
    subgraph T2["Thread 2"]
      S2[Stack]
      C2[PC + Registers]
    end
    T1 --> Heap
    T2 --> Heap
  end

Key point: Sharing the heap makes communication easy—and makes races possible.


4) Why we use threads anyway: the 3 big benefits

1) Exploit multi-core

Multiple runnable threads can run simultaneously on multiple cores.

2) Better throughput via I/O overlap

A single thread blocks waiting for I/O. With concurrency, another thread can run while one waits.

3) Simpler modeling (sometimes)

It can be easier to think:

  • “Each user request is an independent activity”

  • “Each background task is an independent activity”

  • “UI events are handled separately from slow work”

Frameworks often give you this “simple model” while hiding scheduling.


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

Chapter 1’s sneaky truth: you’re already writing concurrent programs because the platform/framework is.

Common sources of “hidden concurrency”

  • Servlet containers: multiple requests invoke your code concurrently.

  • RMI: remote calls can overlap on the server.

  • Swing/AWT: event-driven and asynchronous.

  • Timers/schedulers: background task execution.

  • JVM internals: background threads exist (GC, runtime work).

So even “normal Java code” can be called from multiple threads.


6) The 3 concurrency hazard families (Chapter 1’s core warning)

JCIP gives a super practical taxonomy:

flowchart TB
  C[Concurrency] --> S[Safety: nothing bad happens<br/>Correctness]
  C --> L[Liveness: something good eventually happens<br/>Progress]
  C --> P[Performance: it works but may be slower<br/>Overhead & contention]

Let’s make each one concrete.


6A) Safety hazards (correctness): “nothing bad happens”

Safety problems show up as:

  • race conditions

  • broken invariants

  • corrupted state

  • “works 999 times, fails once” bugs

Example 1: count++ is not atomic

class Counter {
  private int count = 0;
  public void inc() { count++; } // read → add → write (can interleave)
  public int get() { return count; }
}

Two threads can lose updates because increments overlap.

Fix options you’ll see later:

Example fix:

import java.util.concurrent.atomic.AtomicInteger;

class Counter {
  private final AtomicInteger count = new AtomicInteger();
  public void inc() { count.incrementAndGet(); }
  public int get() { return count.get(); }
}

Example 2: “check-then-act” race

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

Two threads can both pass the check and both insert.


6B) Liveness hazards (progress): “something good eventually happens”

Liveness failures include:

  • deadlock (mutual waiting)

  • starvation (one thread never gets scheduled/lock)

  • livelock (lots of motion, no progress)

Deadlock example (classic lock ordering bug)

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) { } }
});

Diagram: deadlock cycle

flowchart LR
  T1[Thread 1] -->|holds| A[Lock A]
  T1 -->|waits for| B[Lock B]
  T2[Thread 2] -->|holds| B
  T2 -->|waits for| A

6C) Performance hazards: “threads can make you slower”

This is the point you insisted on (and it’s one of the most real-world parts of Chapter 1):

Threads can bring net performance gain, but they introduce runtime overhead.
If you overdo threads or coordinate poorly, performance collapses.

The big overhead sources

1) Context switches

The scheduler suspends one thread, resumes another:

  • save/restore execution context (registers, PC, stack metadata)

  • scheduler bookkeeping

2) Loss of locality (cache pain)

Frequent switching means:

  • more cache misses

  • more time reloading data

  • less time doing real work

3) CPU time spent scheduling instead of running

Too many runnable threads → CPU becomes a “thread juggler.”

4) Contention & coordination overhead

Locks/queues shared by many threads:

  • waiting, wakeups, convoying

  • bad tail latency

5) Memory overhead per platform thread

Each OS thread has stack + runtime overhead.

Diagram: where the CPU time goes when you over-thread

CPU time →
[work][context switch][cache refill][work][scheduler][work][lock wait]...

Example: thread-per-client can explode

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

7) “Simplified handling of async events” (how real systems avoid raw thread chaos)

A big reason concurrency is usable in practice: we structure it.

Instead of “new Thread for everything,” we do:

Pattern: events → queue → bounded workers

flowchart LR
  UI[UI click / HTTP request / Timer tick] --> Q[Task Queue]
  Q --> Pool[Thread Pool / Executor]
  Pool --> Result[Response / Update / Next Task]

Java example: ExecutorService

ExecutorService pool = Executors.newFixedThreadPool(32);

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

This is “simplified async event handling” in one line:

  • you submit tasks, not threads

  • concurrency is bounded and manageable

Async composition: CompletableFuture

CompletableFuture
  .supplyAsync(this::fetchData, pool)
  .thenApply(this::transform)
  .thenAccept(this::respond);

This helps you model “what happens next” without blocking the caller.


8) Non-blocking I/O and Unix select() / poll() (and why Java NIO exists)

When you have many connections, thread-per-connection often becomes expensive.

Unix solved “watch many connections efficiently” with I/O multiplexing:

select(): “wait until some fd is ready”

select() lets a program monitor multiple file descriptors until one becomes “ready.” A descriptor is ready if an I/O operation (read/write) will not block. (man7.org)

poll(): similar concept, different interface

poll() also waits for file descriptors to become ready; “ready” means the requested operation will not block. (Arch Manual Pages)

epoll (Linux): designed to scale

Linux epoll performs a similar task to poll but “scales well to large numbers of watched file descriptors.” (man7.org)

kqueue (BSD/macOS): kernel event notification mechanism

kqueue provides a generic event notification mechanism based on kernel “filters.” (man.freebsd.org)


9) Java NIO: the Java face of select/poll/epoll/kqueue

Java NIO gives you a Selector: a “multiplexor of SelectableChannel objects.” (Oracle Docs)

Diagram: blocking vs non-blocking server model

flowchart LR
  subgraph Blocking["Blocking I/O (thread-per-connection)"]
    C1[Client 1] --> T1[Thread 1<br/>blocks on read()]
    C2[Client 2] --> T2[Thread 2<br/>blocks on read()]
    C3[Client 3] --> T3[Thread 3<br/>blocks on read()]
  end

  subgraph NonBlocking["Non-blocking I/O (Selector/event loop)"]
    Many[Many clients] --> Sel[Selector + event loop<br/>(readiness notifications)]
    Sel --> CPU[Optional worker pool<br/>for CPU-heavy tasks]
  end

Minimal Java NIO skeleton (teaching version)

Selector selector = Selector.open(); // uses default provider

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

while (true) {
  selector.select(); // block until at least one 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...
    }
  }
}

“Under the hood” proof (future-proof + concrete)

OpenJDK literally includes:

  • EPollSelectorImpl: “implementation of Selector … uses the epoll event notification facility.” (GitHub)

  • On Linux, the default selector provider wiring uses EPollSelectorProvider. (cocalc.com)

  • KQueueSelectorImpl: “Implementation of Selector using FreeBSD / Mac OS X kqueues.” (cr.openjdk.org)

So conceptually:

  • Unix gives you select/poll/epoll/kqueue

  • Java gives you Selector

  • The JDK maps to platform mechanisms internally


10) Swing EDT: responsiveness + thread safety by confinement

Swing apps are asynchronous by nature: users can click anytime, and they expect the UI to respond promptly.

Swing’s rule is strict:

“All other Swing component methods must be invoked from the event dispatch thread.” (Oracle Docs)

Diagram: Swing EDT + worker thread

flowchart LR
  User[User action] --> EDT[Event Dispatch Thread]
  EDT -->|must be quick| UI[Update UI]
  EDT -->|offload slow work| Worker[Background Thread]
  Worker -->|post result| EDT

Example: UI freeze (bad)

button.addActionListener(e -> {
  doSlowNetworkCall();  // blocks EDT → UI freezes
  label.setText("Done");
});

Example: correct pattern (good)

button.addActionListener(e -> {
  new Thread(() -> {
    String result = doSlowNetworkCall();
    SwingUtilities.invokeLater(() -> label.setText(result));
  }).start();
});

11) RMI: can the same remote method be called concurrently?

Yes. Oracle’s RMI spec is blunt:

“Since remote method invocation on the same remote object may execute concurrently, a remote object implementation needs to make sure its implementation is thread-safe.” (Oracle Docs)

Diagram: concurrent RMI invocations

sequenceDiagram
  participant C1 as Client 1
  participant C2 as Client 2
  participant R as RMI Runtime (Server)
  participant O as Remote Object

  C1->>R: invoke method()
  C2->>R: invoke method()

  par concurrent execution
    R->>O: method() on Thread A
  and
    R->>O: method() on Thread B
  end

  O-->>R: return
  R-->>C1: result
  O-->>R: return
  R-->>C2: result

Implication: Treat remote objects like servlets: assume multiple threads can call them and protect shared mutable state.


12) “Future-proof” update: what changed since JCIP, what never changes

JCIP Chapter 1 is still correct because the problem categories haven’t changed:

  • Safety

  • Liveness

  • Performance

What changed is we now have better tools.

Virtual threads (Java 21)

Virtual threads are lightweight threads meant to dramatically reduce the effort of writing high-throughput concurrent apps. (OpenJDK)

Why this matters for the chapter’s story:

  • It brings back “thread-per-task” readability for many I/O-heavy workloads

  • While still requiring you to think about shared state safety, liveness, and performance

Structured concurrency (still evolving)

Structured concurrency is about treating a group of subtasks as a unit (scope), improving cancellation and observability. The JEP describes StructuredTaskScope for structuring concurrent subtasks. (OpenJDK)

Future-proof rule: Even if APIs change, the core reasoning (shared state + hazards + overhead) stays.


Final recap (Chapter 1 distilled into one slide)

If your viewers remember only this, they’ll be set for Chapter 2:

  1. Multicore made concurrency non-optional.

  2. OS multitasking evolved for utilization, fairness, convenience.

  3. Threads share heap memory → easy communication, easy races.

  4. Threads help with throughput, I/O overlap, responsiveness, and modeling.

  5. Concurrency shows up “under the hood” (Servlets, Swing, RMI, timers, JVM).

  6. Hazards fall into Safety, Liveness, Performance.

  7. Performance hazards are real: context switches, cache locality loss, scheduling/coordination overhead.

  8. Non-blocking I/O exists to avoid “thread-per-connection” scalability limits.

  9. Unix select/poll and Linux epoll/BSD kqueue are the OS foundation. (man7.org)

  10. Java NIO Selector is the Java abstraction over readiness multiplexing. (Oracle Docs)


YouTube-friendly slide plan (you can literally follow this)

  1. Hook: Tea kettle analogy (no diagram)

  2. Multi-core shift: Core boxes diagram

  3. OS motivations: utilization/fairness/convenience (3 bullet icons)

  4. Process vs thread: Mermaid process/thread diagram

  5. Benefits: throughput + responsiveness + modeling (3 callouts)

  6. Hazards triangle: Safety/Liveness/Performance diagram

  7. Safety example: count++ race code

  8. Liveness example: deadlock cycle diagram

  9. Performance: CPU time strip diagram + “too many threads” snippet

  10. Blocking vs non-blocking: server model diagram

  11. Unix readiness: select/poll/epoll/kqueue bullets with one visual

  12. Swing EDT: EDT diagram

  13. RMI concurrency: sequence diagram

  14. Modern Java: virtual threads + structured concurrency (one slide)

  15. Wrap-up checklist: final recap slide