A self-contained Java concurrency study guide: a hand-copyable cheat sheet, from-scratch explanations, runnable code for every concept, and a graded hands-on lab. Built for Java 17+ (works on Java 8+).
High-density cheat sheet. Monospace blocks copy cleanly into a notebook; doodle lines are ~10-second sketches.
CONCURRENCY ISSUES (shared data + many threads) 2 bug types: 1. Thread interference (RACE CONDITION) -> atomicity problem 2. Memory consistency error -> visibility problem RACE CONDITION = 2+ threads read+write shared data at once -> unpredictable cause: op NOT atomic atomic = single step, all-or-nothing, no interrupt i++ = read -> modify -> write (3 steps!) -> thread slips between Inventory: incr + decr on shared obj -> lost update -> wrong final final SHOULD be 1 or -1; race makes ops overwrite each other CRITICAL SECTION = code touching shared resource, must be protected race = threads enter critical section WITHOUT syncdoodle: two arrows -> one box "items" -> collide = ⚡ (2 threads hit same var) doodle: i++ = [read]->[+1]->[write], red gap between read & write = intruder slips in
SYNCHRONIZED (fix for races)
every Java object -> has a MONITOR (intrinsic lock)
enter sync region -> ACQUIRE monitor; others BLOCK till RELEASE
lock is PER INSTANCE (diff objs = diff locks, no blocking)
gives: mutual exclusion + visibility (BOTH bugs fixed)
2 forms:
METHOD -> locks whole method (this)
BLOCK -> synchronized(obj){...} shortest hold = better
Inventory fix: sync getItems/increment/decrement
-> 1 thread at a time, read sees latest write
doodle: box = object, key on door, 1 stick figure inside, line waiting outside
MEMORY CONSISTENCY ERROR
= threads have inconsistent views; write not propagated -> stale read
SmartFan: main flips fanOn, fanThread never sees -> never stops
causes: uncoordinated updates · no sync · instruction reorder ·
cache coherence · delayed visibility · optimistic compiler
(root: per-core caches + JVM may hoist read out of loop)
VOLATILE
visibility -> write seen by all threads immediately (main mem)
non-blocking -> unlike synchronized, visibility ONLY
NOT atomic -> i++ still needs sync
use: shared var, care only about visibility (flags, e.g. fanOn)
doodle: main-mem box ↕ 2 core-cache boxes; volatile = arrows forcing sync ↕↕
| volatile | synchronized | |
|---|---|---|
| Gives | visibility | visibility + atomicity |
| Blocks? | no | yes |
| Scope | 1 variable | block of ops |
RULE OF THUMB
every shared var modified by >=1 thread ->
EITHER lock-guarded (synchronized) -> mutual exclusion + atomic
OR volatile -> happens-before, visibility
shortcut: compound op? -> lock | just a flag? -> volatile
SYNCHRONIZED LIMITS (-> why advanced locks exist)
1. no flexible acquire -> held till exit, cant try (bathroom lock)
2. no fairness -> not request order, starvation (swing bully)
3. not interruptible -> blocked thread cant be interrupted (stuck in line)
4. no explicit lock/unlock -> implicit at block edges (auto door)
REENTRANT LOCK (java.util.concurrent.locks)
flexible alt to synchronized:
tryLock() · tryLock(time,unit) · lockInterruptibly() · fairness
must unlock() explicitly -> lock(); try{...} finally{unlock();}
reentrant = same thread can re-acquire its own lock
SEMAPHORE (java.util.concurrent)
lock = 1 thread; semaphore = up to N threads (permit counter)
parking lot: 3 spots, car parks -> counter--, full -> wait
init(1) = binary semaphore ~= lock
doodle: parking lot, 3 boxes, 2 cars parked + 1 free, line waiting at gate
| Tool | Use / hook |
|---|---|
| ReadWriteLock | many readers OR 1 writer (library reading vs checkout) |
| CountDownLatch | wait for event, count→0 then go (starting pistol, one-shot) |
| CyclicBarrier | all reach point then go; resets/reusable (hikers checkpoint) |
latch = counts DOWN once (one-shot) | barrier = CYCLIC (reusable)
wait() / notify() / notifyAll() — on Object, via shared obj
wait() -> thread waits till woken; 0 CPU; RELEASES monitor
notify() -> wake 1 waiting thread
notifyAll() -> wake ALL waiting threads
MUST hold monitor (be in synchronized) else IllegalMonitorStateException
best practice: wait() inside while(cond), not if
PRODUCER-CONSUMER (shared buffer)
producer -> puts data | consumer -> takes data
problems: OVERRUN (prod too fast) · UNDERRUN (cons too fast)
Solution A — wait/notify:
full -> producer.wait(); after produce -> notify() consumer
empty -> consumer.wait(); after consume -> notify() producer
Solution B — BlockingQueue:
thread-safe, auto-blocks, no manual wait/notify
impls: ArrayBlockingQueue, LinkedBlockingQueue -> prefer in real code
doodle: [Producer] -> [ buffer ▢▢▢ ] -> [Consumer]; full=prod waits, empty=cons
waits
One subsection per concept. Each opens with a one-line claim, builds the idea from scratch, then flags why it matters.
Single-threaded code is predictable: statements run top to bottom. The moment two threads touch the same mutable data, correctness stops depending on your code's written order and starts depending on timing, CPU caches, and compiler optimizations.
Interference is like two people editing the same spreadsheet cell and overwriting each other. Consistency is like editing the cell while my screen still shows the old number because it never refreshed. A program can have one, the other, or both.
synchronized/locks fix atomicity,
volatile/synchronized fix visibility. Diagnosing which bug you have tells you
which tool to reach for.In the Inventory Counter demo, one IncrementingThread and one
DecrementingThread share a single counter. Run it repeatedly and the final value keeps
changing instead of settling. The cause: increment() and decrement() are
not atomic.
An atomic operation finishes in one indivisible step — it can't be interrupted or left
half-done. [+] Although items++ looks like one line, the CPU
runs three steps: read the value → add one → write it
back. A second thread can interleave between any two steps. If both read 0 before either
writes, one write overwrites the other and an update is lost. The deck's walkthrough shows the
result landing on 1 or -1 wrongly — logically it should be exactly one of
those depending on order, but the race makes the operations clobber each other.
i++ into
read-modify-write — it's the canonical proof you understand races.Code: InventoryCounter (broken)
Races happen precisely when threads enter a critical section without synchronization. The fix isn't
shorter code — it's ensuring only one thread is inside at a time. Printer analogy: many
people, one printer; with no coordination jobs overlap and pages mix. The rule "one person at a time"
guarantees correct output. In Java, synchronized enforces that rule.
synchronized keyword & monitorssynchronized uses it so
only one thread runs a guarded region on that object at a time.When a thread enters a synchronized region it acquires the monitor tied to
the object. Any other thread trying to enter the same — or any other — synchronized region on that
same object blocks until release on exit.
Two forms: synchronized methods (lock = the instance, this) and
synchronized blocks (synchronized(obj){…}, lock a chosen span on a chosen
object). [+] Blocks are usually preferred — hold the lock for the
shortest necessary window. Fixing the counter: synchronize getItems(),
increment(), decrement() → one thread at a time, and a read sees the latest
write.
synchronized fixes
both atomicity and visibility for the guarded data.Code: InventoryCounter (synchronized method & block)
The SmartFan demo: a fan thread loops watching a fanOn flag. The main
thread flips it, but the fan thread reads a stale cached copy, never reacts, and the program won't even
terminate. The deck lists six causes: uncoordinated updates, lack of synchronization, instruction
reordering (Java Memory Model), cache coherence issues, delayed visibility, and optimistic compiler
behavior.
[+] Hardware view: each core has its own cache; a write may sit in one
core's cache and not reach others promptly. The JVM may also hoist a repeated read out of a loop into a
register — which is exactly how while(!fanOn) spins forever.
Code: SmartFan (broken) · SmartFan (volatile)
volatile keywordvolatile guarantees visibility — a write is immediately seen by all threads
and reads come from main memory, not a stale cache.It is non-blocking (unlike synchronized) and not atomic —
compound ops like i++ still need a lock. Declaring fanOn volatile fixes
SmartFan: the change is instantly visible, the fan reacts, the program ends.
volatile int count; count++; is
still racy.Code: SmartFan (volatile)
volatile.Lock-guarded → mutual exclusion + atomicity. volatile → a happens-before relationship giving
visibility. [+] Shortcut: compound action on the variable → lock; plain
flag you write and others read → volatile.
synchronizedsynchronized is simple but rigid — four gaps motivate the advanced locks.
| Limitation | Consequence | Analogy |
|---|---|---|
| No flexible acquisition | Held till exit; can't try without blocking | Bathroom lock — everyone waits, even for a quick check |
| No fairness | Not in request order; starvation possible | Swing the stronger kids always grab |
| Not interruptible | Blocked waiter can't be interrupted | Stuck in line, can't step out for an urgent call |
| No explicit lock/unlock | Implicit at block edges; no early/ordered unlock | Door that only locks on entry, unlocks on exit |
java.util.concurrent was built to close.ReentrantLock is a flexible alternative to synchronized that
closes its four gaps.It adds tryLock() (non-blocking attempt), tryLock(time, unit) (bounded wait),
lockInterruptibly() (interruptible acquisition), and configurable
fairness. [+] The cost: you must unlock()
explicitly — always in a finally so it releases even on exceptions. "Reentrant" = the same
thread can re-acquire a lock it already holds without deadlocking itself.
finally { lock.unlock(); } leaks the lock and hangs every other thread. This is the #1
ReentrantLock bug.Code: Restaurant (ReentrantLock)
Parking-lot analogy: three spots, a long line of cars; only three park at once. Each
acquire decrements the counter; when full, newcomers wait for a release. [+] A semaphore of 1 behaves like a lock (binary semaphore).
Use semaphores to throttle concurrency — cap DB connections or in-flight API calls.
Code: ParkingLot (Semaphore)
| Tool | Use case | Analogy |
|---|---|---|
ReadWriteLock |
Many readers OR one writer | Library: many read, one at checkout |
CountDownLatch |
Wait for an event before proceeding | Starting pistol; count→0 then go (one-shot) |
CyclicBarrier |
All threads reach a point before continuing | Hikers' checkpoint; resets & reusable |
Code: CountDownLatch & CyclicBarrier demos
wait() / notify() /
notifyAll()wait() parks the current thread until woken, consuming no CPU, and [+] releases the monitor so others can progress.
notify() wakes one waiter; notifyAll() wakes all. Rule: you
must hold the object's monitor (be inside synchronized on it) or you get
IllegalMonitorStateException.
Object (not Thread) because the mechanism is tied to the object's
monitor, which every object has. Always wait() inside a while(condition) loop,
not an if, to survive spurious wakeups. Prefer notifyAll() when multiple
conditions share one lock.Code: Producer-Consumer (wait/notify)
The producer adds data; the consumer removes it. Two challenges: buffer overrun (producer too fast → full buffer) and buffer underrun (consumer too fast → empty buffer).
Solution A — wait/notify: producer wait()s when full and
notify()s after producing; consumer wait()s when empty and
notify()s after consuming. Solution B — BlockingQueue: a thread-safe queue
that auto-blocks on full/empty, needs no manual wait/notify; implementations include
ArrayBlockingQueue and LinkedBlockingQueue, and it scales easily to many
producers/consumers.
BlockingQueue in real code — manual wait/notify is easy to deadlock or mis-signal. Use the
raw mechanism to prove understanding or for behavior the queue can't express.Code: Producer-Consumer (wait/notify) · Producer-Consumer (BlockingQueue)
i++ is not atomic — it's read-modify-write.synchronized = per-instance monitor → mutual exclusion + visibility.volatile = visibility only, non-blocking, not atomic; use for flags.synchronized limits → ReentrantLock (tryLock/fairness/interruptible), Semaphore (N
permits).Real, compilable code for every concept the notes reference. Standard library only. Each has a filename, run command, inline comments, and expected output.
public class InventoryCounter { private int items = 0; // shared mutable state public void increment() { items++; } // read-modify-write (NOT atomic) public void decrement() { items--; } public int getItems() { return items; } public static void main(String[] a) throws InterruptedException { InventoryCounter c = new InventoryCounter(); // each thread does 1,000,000 ops on the SAME object Thread inc = new Thread(() -> { for (int i=0;i<1_000_000;i++) c.increment(); }); Thread dec = new Thread(() -> { for (int i=0;i<1_000_000;i++) c.decrement(); }); inc.start(); dec.start(); inc.join(); dec.join(); // wait for both to finish System.out.println("items = " + c.getItems()); // expect 0... rarely is } }
Why: 1M increments and 1M decrements should cancel to 0. But items++ and
items-- each read-modify-write; when they interleave, updates are lost, so the total
drifts. The exact number changes per run because thread scheduling is nondeterministic.
synchronizedpublic class SyncCounter { private int items = 0; // METHOD form: lock is 'this' instance's monitor public synchronized void increment() { items++; } public synchronized void decrement() { items--; } public synchronized int getItems() { return items; } // BLOCK form does the same thing, finer-grained: private final Object lock = new Object(); public void incrementBlock() { synchronized (lock) { items++; } } public static void main(String[] a) throws InterruptedException { SyncCounter c = new SyncCounter(); Runnable up = () -> { for(int i=0;i<1_000_000;i++) c.increment(); }; Runnable dn = () -> { for(int i=0;i<1_000_000;i++) c.decrement(); }; Thread t1=new Thread(up), t2=new Thread(dn); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("items = " + c.getItems()); } }
Why: only one thread can hold the instance's monitor at a time, so each
items++/-- completes fully before the other thread runs — no lost updates. Synchronizing
getItems() too guarantees the read sees the latest write.
public class SmartFan { private static boolean fanOn = false; // shared flag, NOT volatile public static void main(String[] a) throws InterruptedException { Thread fan = new Thread(() -> { while (!fanOn) { /* busy-wait: may read a CACHED false forever */ } System.out.println("Fan is ON! Cooling the room..."); }); fan.start(); Thread.sleep(1000); System.out.println("User: Turning the fan ON..."); fanOn = true; // main writes; fan thread may never see it } }
Why: the JIT can hoist fanOn into a register and loop on a stale
false, never re-reading main memory. The write is real but invisible to the fan thread.
[+] On some JVMs/timings it may eventually print — visibility without
volatile is "undefined," not "guaranteed broken." That's why you can't rely on it.
volatilepublic class SmartFanVolatile { private static volatile boolean fanOn = false; // visibility guaranteed public static void main(String[] a) throws InterruptedException { Thread fan = new Thread(() -> { while (!fanOn) { } // now always re-reads from main memory System.out.println("Fan is ON! Cooling the room..."); }); fan.start(); Thread.sleep(1000); System.out.println("User: Turning the fan ON..."); fanOn = true; // instantly visible to fan thread fan.join(); } }
Why: volatile forces every read to come from main memory and every write to
flush to it, establishing happens-before. The fan thread now sees true immediately and
exits cleanly.
tryLock & the finally idiomimport java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.TimeUnit; public class Restaurant { private static final ReentrantLock table = new ReentrantLock(true); // true = fair static void dine(String who) { try { // try up to 1s to get the table; don't block forever if (table.tryLock(1, TimeUnit.SECONDS)) { try { System.out.println(who + " is dining"); Thread.sleep(300); } finally { table.unlock(); // ALWAYS release in finally } } else { System.out.println(who + " gave up waiting"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public static void main(String[] a) { for (String n : new String[]{"Ann","Bo","Cy"}) new Thread(() -> dine(n)).start(); } }
Why: tryLock(1, SECONDS) gives the flexibility synchronized
lacks — a bounded wait instead of blocking forever. The finally { unlock(); } is mandatory;
without it a thrown exception would leak the lock. new ReentrantLock(true) grants the table
in request order (fairness).
import java.util.concurrent.Semaphore; public class ParkingLot { private static final Semaphore spots = new Semaphore(3); // 3 permits static void park(int car) { try { spots.acquire(); // take a permit (waits if 0 free) System.out.println("Car "+car+" parked. Free="+spots.availablePermits()); Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { System.out.println("Car "+car+" leaving"); spots.release(); // return the permit } } public static void main(String[] a) { for (int i=1;i<=6;i++){ int c=i; new Thread(()->park(c)).start(); } } }
Why: the semaphore caps concurrency at 3. Cars 4–6 block on acquire() until
a parked car calls release(). Replace 3 with 1 and it behaves
like a lock.
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; public class LatchAndBarrier { public static void main(String[] a) throws Exception { // --- CountDownLatch: 3 runners wait for one starting pistol --- CountDownLatch pistol = new CountDownLatch(1); for (int i=1;i<=3;i++){ int r=i; new Thread(()->{ try{ pistol.await(); // block until count==0 System.out.println("Runner "+r+" GO"); }catch(InterruptedException e){} }).start(); } Thread.sleep(300); System.out.println("BANG!"); pistol.countDown(); // one-shot release // --- CyclicBarrier: 3 hikers must all arrive before moving --- CyclicBarrier checkpoint = new CyclicBarrier(3, () -> System.out.println("All arrived -> move on")); // barrier action for (int i=1;i<=3;i++){ int h=i; new Thread(()->{ try{ Thread.sleep(h*200); System.out.println("Hiker "+h+" at checkpoint"); checkpoint.await(); // wait for the other two }catch(Exception e){} }).start(); } } }
Why: the latch's countDown() fires once and can't be reset — after it hits
0 it stays open. The barrier's await() blocks each hiker until all 3 arrive, runs the
barrier action, then resets so it could be reused for a next checkpoint.
import java.util.LinkedList; import java.util.Queue; public class ProducerConsumer { private final Queue<Integer> buffer = new LinkedList<>(); private final int CAP = 3; public void produce(int v) throws InterruptedException { synchronized (buffer) { // must hold monitor to wait/notify while (buffer.size() == CAP) buffer.wait(); // full -> wait (releases lock) buffer.add(v); System.out.println("produced "+v+" size="+buffer.size()); buffer.notifyAll(); // wake waiting consumer(s) } } public int consume() throws InterruptedException { synchronized (buffer) { while (buffer.isEmpty()) buffer.wait(); // empty -> wait int v = buffer.poll(); System.out.println(" consumed "+v); buffer.notifyAll(); // wake waiting producer(s) return v; } } public static void main(String[] a) { ProducerConsumer pc = new ProducerConsumer(); new Thread(()->{ try{ for(int i=1;i<=5;i++) pc.produce(i); }catch(Exception e){} }).start(); new Thread(()->{ try{ for(int i=1;i<=5;i++){ pc.consume(); Thread.sleep(100);} }catch(Exception e){} }).start(); } }
Why: wait() is called inside while (not if) to
re-check the condition after waking — guarding against spurious wakeups. It releases the buffer's
monitor while waiting so the other side can proceed. notifyAll() wakes the counterpart.
Capacity 3 is never exceeded.
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class ProducerConsumerBQ { public static void main(String[] a) { // capacity 3; put() blocks when full, take() blocks when empty BlockingQueue<Integer> q = new ArrayBlockingQueue<>(3); new Thread(()->{ try{ for(int i=1;i<=5;i++){ q.put(i); // auto-blocks if full System.out.println("put "+i); } }catch(InterruptedException e){} }).start(); new Thread(()->{ try{ for(int i=1;i<=5;i++){ int v=q.take(); // auto-blocks if empty System.out.println(" took "+v); Thread.sleep(100);} }catch(InterruptedException e){} }).start(); } }
Why: ArrayBlockingQueue handles all locking internally. put()
blocks the producer when full; take() blocks the consumer when empty. Same behavior as #8
with ~half the code and no chance of a missed-notification deadlock — the reason to prefer it in
production.
Predict the output before running each one — concurrency intuition is built by being wrong and seeing why.
# 1. Check Java (need 11+, 17+ ideal). Single-file run needs 11+. java -version # 2. Make a lab folder mkdir -p ~/concurrency-lab && cd ~/concurrency-lab # 3. Each exercise = one .java file. Run it directly (no manual compile): java Exercise1.java # (Older Java? Compile then run:) javac Exercise1.java && java Exercise1
Goal: See a non-atomic counter produce a wrong total.
int count = 0 in a class.count++ 1,000,000 times.join() both, then print count.public class Exercise1 { static int count = 0; public static void main(String[] a) throws InterruptedException { Runnable r = () -> { for(int i=0;i<1_000_000;i++) count++; }; Thread a1=new Thread(r), b=new Thread(r); a1.start(); b.start(); a1.join(); b.join(); System.out.println(count); // expect 2,000,000; almost never is } }
Lost updates: both threads read the same value before either writes back, so increments vanish.
synchronizedGoal: Make Exercise 1 deterministic.
count++ into a synchronized method inc().inc() in their loop.public class Exercise2 { static int count = 0; static synchronized void inc() { count++; } // critical section public static void main(String[] a) throws InterruptedException { Runnable r = () -> { for(int i=0;i<1_000_000;i++) inc(); }; Thread a1=new Thread(r), b=new Thread(r); a1.start(); b.start(); a1.join(); b.join(); System.out.println(count); // always 2,000,000 } }
Always correct now, but slower than Ex1 — lock acquire/release on every increment adds overhead. Correctness over raw speed.
Goal: Reproduce a visibility bug, then fix it with
volatile.
while(running){} on a non-volatile
boolean running=true.running=false and prints "stop requested".volatile to running and rerun.volatile? Does
behavior change if you add a System.out.println() inside the loop — and why might that
"accidentally fix" it?public class Exercise3 { static volatile boolean running = true; // remove 'volatile' to see the bug public static void main(String[] a) throws InterruptedException { Thread w = new Thread(() -> { long n=0; while (running) n++; System.out.println("worker stopped after "+n+" loops"); }); w.start(); Thread.sleep(1000); running = false; System.out.println("stop requested"); } }
Without volatile the worker may loop forever on a cached
true. A println inside the loop can mask the bug because I/O
includes internal synchronization that flushes memory — a misleading "fix," never rely on
it.
Goal: Allow at most 2 of 5 workers to "download" at once.
Semaphore(2).acquire() → print "downloading" → sleep 500ms →
release() in finally.availablePermits() as you go.release()?import java.util.concurrent.Semaphore; public class Exercise4 { static final Semaphore s = new Semaphore(2); static void work(int id){ try{ s.acquire(); System.out.println("worker "+id+" downloading (free="+s.availablePermits()+")"); Thread.sleep(500); }catch(InterruptedException e){Thread.currentThread().interrupt();} finally{ System.out.println("worker "+id+" done"); s.release(); } } public static void main(String[] a){ for(int i=1;i<=5;i++){int id=i;new Thread(()->work(id)).start();} } }
Only 2 download simultaneously. Forgetting release()
permanently shrinks available permits — eventually everything blocks forever (a permit
leak).
Goal: Build a bounded buffer (capacity 5) with one producer and one
consumer using only wait()/notify().
Queue<Integer> + capacity 5, guarded by synchronized.
while (not if) around wait(); call
notifyAll() after each op.while to
if? What happens if you remove the synchronized block around
wait()?import java.util.*; public class Exercise5 { static final Queue<Integer> buf = new LinkedList<>(); static final int CAP = 5; static void produce(int v) throws InterruptedException { synchronized(buf){ while(buf.size()==CAP) buf.wait(); // while, not if! buf.add(v); System.out.println("P "+v+" (size "+buf.size()+")"); buf.notifyAll(); } } static void consume() throws InterruptedException { synchronized(buf){ while(buf.isEmpty()) buf.wait(); System.out.println(" C "+buf.poll()); buf.notifyAll(); } } public static void main(String[] a){ new Thread(()->{try{for(int i=1;i<=10;i++)produce(i);}catch(Exception e){}}).start(); new Thread(()->{try{for(int i=1;i<=10;i++){consume();Thread.sleep(80);}}catch(Exception e){}}).start(); } }
if instead of while → a spurious/early wakeup
proceeds on a still-full/empty buffer, corrupting it. Removing synchronized →
IllegalMonitorStateException, because wait/notify require holding the monitor.
Goal: Achieve the same behavior with zero manual synchronization, then compare line counts.
ArrayBlockingQueue<>(5).put(); consumer calls take().synchronized/wait/notify code.import java.util.concurrent.*; public class Exercise6 { public static void main(String[] a){ BlockingQueue<Integer> q = new ArrayBlockingQueue<>(5); new Thread(()->{try{for(int i=1;i<=10;i++){q.put(i);System.out.println("P "+i);}}catch(InterruptedException e){}}).start(); new Thread(()->{try{for(int i=1;i<=10;i++){System.out.println(" C "+q.take());Thread.sleep(80);}}catch(InterruptedException e){}}).start(); } }
~25 lines shorter and no chance of a missed notification. The queue handled capacity blocking, monitor management, and signalling — everything you hand-coded in #5. Ship this one.
count++ into read-modify-write.volatile and why
println can mask it.while (not if) and
why the monitor must be held.BlockingQueue over hand-rolled
wait/notify in production and justify it.Built from the original session deck · content marked [+] was added beyond the source · Java 17+, standard library only.