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)
Heap objects / shared memory (most important!)
Same address space
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:
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:
synchronizedimmutability / confinement
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/kqueueJava gives you
SelectorThe 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:
Multicore made concurrency non-optional.
OS multitasking evolved for utilization, fairness, convenience.
Threads share heap memory → easy communication, easy races.
Threads help with throughput, I/O overlap, responsiveness, and modeling.
Concurrency shows up “under the hood” (Servlets, Swing, RMI, timers, JVM).
Hazards fall into Safety, Liveness, Performance.
Performance hazards are real: context switches, cache locality loss, scheduling/coordination overhead.
Non-blocking I/O exists to avoid “thread-per-connection” scalability limits.
Unix
select/polland Linuxepoll/BSDkqueueare the OS foundation. (man7.org)Java NIO
Selectoris the Java abstraction over readiness multiplexing. (Oracle Docs)
YouTube-friendly slide plan (you can literally follow this)
Hook: Tea kettle analogy (no diagram)
Multi-core shift: Core boxes diagram
OS motivations: utilization/fairness/convenience (3 bullet icons)
Process vs thread: Mermaid process/thread diagram
Benefits: throughput + responsiveness + modeling (3 callouts)
Hazards triangle: Safety/Liveness/Performance diagram
Safety example:
count++race codeLiveness example: deadlock cycle diagram
Performance: CPU time strip diagram + “too many threads” snippet
Blocking vs non-blocking: server model diagram
Unix readiness:
select/poll/epoll/kqueuebullets with one visualSwing EDT: EDT diagram
RMI concurrency: sequence diagram
Modern Java: virtual threads + structured concurrency (one slide)
Wrap-up checklist: final recap slide