J
Java Crash Course
15 Marks — Concurrency from zero, with anchors to make it stick

How to Read This

Your exam is about Java concurrency — threads sharing data safely. This guide builds up from "what is a thread" to "design a thread-safe print spooler." Every section uses only what you've already seen. Look for the purple Memory Anchor boxes — they'll give you jokes, mnemonics, and vivid images to pin these concepts in your brain.

1. Why Concurrency Is Hard — The One Problem

In C, you write code and it runs line by line. With concurrency, multiple threads run the same code at the same time on shared data. And that's where everything breaks.

// Looks innocent. Is actually broken.
int balance = 100;

// Thread A:                    // Thread B (at the exact same time):
balance = balance + 50;          balance = balance - 30;
//                               EXPECTED: 100 + 50 - 30 = 120
//                               ACTUAL: could be 150, 70, or 120 (random!)

Why? Because balance = balance + 50 is actually three steps: (1) read balance, (2) add 50, (3) write back. If Thread B reads between steps 1 and 3 of Thread A, it sees the OLD value. This is a race condition.

Memory Anchor: Race Condition
Two people editing the same Google Doc, both offline. Person A adds a paragraph. Person B deletes a paragraph. They both sync. Result: chaos. That's a race condition — the outcome depends on who goes first, and you can't control that.

The entire Java concurrency toolkit exists to solve this one problem: how do you let multiple threads share data without corrupting it?

2. Java Syntax Survival Kit (for C/C++ Programmers)

You already know how to code. Here's what's different in Java.

Classes & Methods

public class Main {
    private int count = 0;             // instance variable
    private final int MAX = 10;        // final = const

    public void increment() { count++; }
    public int getCount() { return count; }

    public static void main(String[] args) {   // entry point
        Main obj = new Main();
        obj.increment();
        System.out.println(obj.getCount());     // prints 1
    }
}

C++ → Java Cheat Sheet

C++JavaNote
int* ptrNo pointersEverything is a reference (like shared_ptr)
delete objGarbage collectedNo manual free, ever
#include <vector>import java.util.*;
std::cout <<System.out.println()
cin >> xScanner sc = new Scanner(System.in); sc.nextInt();
const intfinal intAlso works on references (can't reassign, but can mutate object)
virtual void f() = 0interfaceInterface = pure virtual class
Templates <T>Generics <T>Similar syntax but no specialization
enum { A, B }enum Status { A, B }Java enums are full objects (can have methods!)

Types you'll see in the exam

// Interface: contract that classes must implement
public interface Spooler {
    long submitBlocking(PrintJob job) throws InterruptedException;
    boolean cancel(long jobId);
}

// Enum: fixed set of constants
public enum JobStatus { QUEUED, PRINTING, DONE, CANCELLED }

// Record: immutable data class (like a struct with no setters)
public record SpoolerMetrics(long totalSubmitted, long totalCompleted) {}

// Regular class with validation
public final class PrintJob {
    private final String owner;
    private final int pages;
    public PrintJob(String owner, int pages) {
        if (pages <= 0) throw new IllegalArgumentException("pages must be > 0");
        this.owner = owner; this.pages = pages;
    }
    public String owner() { return owner; }
    public int pages() { return pages; }
}

3. Threads — Running Code in Parallel

A thread is an independent line of execution. Your program starts with one thread (the main thread). You can create more.

// Create a thread using a lambda (Runnable)
Thread t = new Thread(() -> {
    System.out.println("Hello from a different thread!");
});

t.start();   // launches a NEW thread that runs the lambda
t.join();    // main thread WAITS for t to finish
EXAM TRAP: start() vs run()
t.start() = creates a new thread and runs the code there.
t.run() = calls the method normally in the CURRENT thread. No new thread is created.

Always use start(). Using run() defeats the entire purpose.
Memory Anchor: start() vs run()
start() = hiring a new employee and giving them the task.
run() = doing it yourself while the employee stands there watching. Don't be that manager.

Thread lifecycle

MethodWhat it does
t.start()Launch the thread (can only be called ONCE per Thread object)
t.join()Block the current thread until t finishes
t.interrupt()Politely ask t to stop (sets a flag; doesn't force-kill)
Thread.sleep(ms)Current thread sleeps for ms milliseconds
Thread.currentThread()Returns the Thread object for whoever is running right now

4. synchronized — The Bathroom Lock

The fix for race conditions: make sure only one thread at a time can touch the shared data. That's what synchronized does.

Think of it like: a single-occupancy bathroom. There's one key hanging on the door. To go in, you take the key. When you're done, you hang it back. If someone else took the key, you WAIT outside until they're done. That key is the lock. The bathroom is the critical section.

Method-level synchronized

class BankAccount {
    private int balance = 100;

    synchronized void deposit(int amount) {     // grabs lock on "this"
        balance += amount;                        // only one thread in here at a time
    }                                             // releases lock on "this"

    synchronized void withdraw(int amount) {    // same lock as deposit!
        balance -= amount;
    }

    synchronized int getBalance() {              // yes, reads need locking too
        return balance;
    }
}

When you mark a method synchronized, Java uses this (the object itself) as the lock. All synchronized methods on the same object share the same lock. So if Thread A is inside deposit(), Thread B can't enter withdraw() on the same object until A finishes.

Block-level synchronized

void doStuff() {
    // ... some thread-safe code ...
    synchronized (this) {            // same as synchronized method
        balance += 50;              // only this part is locked
    }
    // ... more thread-safe code ...
}

Block-level is useful when only PART of your method needs protection. Less time holding the lock = less waiting for other threads.

Memory Anchor: The "this" Lock
Every Java object has an invisible lock built into it — like every house has a front door lock. When you say synchronized(this), you're using YOUR object's lock. All synchronized methods on the same object = same lock = same bathroom = one at a time.
synchronized Rules
  • synchronized is reentrant: if Thread A holds the lock and calls another synchronized method on the same object, it doesn't deadlock (it already has the key)
  • Lock is released when you exit the synchronized block/method — even if an exception is thrown
  • Reading shared data needs synchronized too! Without it, you might see a stale cached value

5. wait() + notifyAll() — "Wake Me When It's Ready"

synchronized prevents two threads from being in the critical section at the same time. But what if a thread gets in and finds the data isn't ready yet? (Queue is empty, buffer is full, etc.) It could spin-wait in a loop, but that wastes CPU. Enter: wait().

Think of it like: you walk into the bathroom (grab the lock). But there's no toilet paper. Instead of standing there hogging the room, you step outside and sit on a bench (wait()). When someone refills the paper, they shout "PAPER'S BACK!" (notifyAll()). Everyone on the bench wakes up, and ONE person goes back in to check.

What wait() actually does (3 things!)

  1. Releases the lock (steps out of the bathroom)
  2. Puts the thread to sleep (sits on the bench)
  3. When notified, re-acquires the lock before continuing (goes back in)

The Core Pattern

class SharedResource {
    private int value = 0;

    synchronized void waitForValue(int target) throws InterruptedException {
        while (value != target) {    // 1. Check condition
            wait();                   // 2. Not ready? Release lock, sleep
        }                             // 3. Woke up - loop back, CHECK AGAIN
        // Condition is true AND we hold the lock. Safe to proceed.
    }

    synchronized void setValue(int v) {
        value = v;
        notifyAll();                  // Wake ALL sleeping threads
    }
}
THE THREE IRON RULES — Violate any one and your code is broken

Iron Rule #1: ALWAYS while, NEVER if

Why can't you use if (condition) wait() instead of while?

Memory Anchor: The Pizza Party
You're sleeping on the bench. Someone shouts "PIZZA'S HERE!" (notifyAll). You wake up and rush to the kitchen. But three other people woke up too, and they ate all the pizza before you got there. With if, you'd start eating air. With while, you check the box again — empty? Back to sleep.

There are two reasons while is mandatory:

  1. Competition: Multiple threads wake up, but only one can act. The others must re-check.
  2. Spurious wakeups: Java (and the OS) can wake a thread for NO REASON at all. Rare, but real. while handles it; if doesn't.

Iron Rule #2: ALWAYS notifyAll(), NEVER notify()

Memory Anchor: The Group Chat
notify() = texting ONE random friend "pizza's here." If that friend is already full (wrong thread, wrong condition), nobody comes. Pizza goes cold. Deadlock.

notifyAll() = posting in the group chat. Everyone checks. The hungry one eats. The full ones go back to sleep. Always safe.

Iron Rule #3: wait() and notifyAll() MUST be inside synchronized

You can't step out of a bathroom you never entered. Calling wait() outside synchronized throws IllegalMonitorStateException.

Mental Trace: Two Threads Sharing a Value
Thread A: calls waitForValue(42)
  Enters synchronized, grabs lock
  Checks: value (0) != 42? Yes. Calls wait().
Releases lock. Sleeps on bench.

Thread B: calls setValue(42)
  Enters synchronized, grabs lock (A released it!)
  Sets value = 42
  Calls notifyAll() — wakes Thread A
  Exits synchronized, releases lock

Thread A: wakes up
  Re-acquires lock
  Loops back to while: value (42) != 42? No! Exits loop.
  Continues safely, holding the lock. Done.
The mantra: while-wait, change-notifyAll, always-synchronized. Tattoo it on your brain.

6. volatile — "No Secret Diaries"

Here's a subtle problem: even reading shared data across threads can break without proper synchronization.

Why? CPU caches.

Each CPU core has its own cache — a private copy of recently-used memory. When Thread A writes closed = true, that value might sit in Core 1's cache and never make it to main memory. Thread B on Core 2 keeps reading closed = false from its own cache. Forever.

Memory Anchor: The Roommate's Whiteboard
Two roommates share a whiteboard in the kitchen for the grocery list. But each has a personal notepad where they copy the list. Without volatile, they only look at their notepad — one adds "milk" to the whiteboard, the other never sees it because they're reading their stale copy.

volatile = "ALWAYS check the whiteboard. Never trust your notepad."

The fix: volatile

private volatile boolean closed = false;    // written by Thread A

// Thread B (dispatcher loop):
while (!closed) {    // without volatile, this might loop FOREVER
    // ... do work ...
}

// Thread A (shutdown):
closed = true;       // volatile guarantees B sees this immediately

volatile guarantees that every read goes to main memory (the whiteboard) and every write is flushed to main memory immediately. No caching.

When to use volatile

Use volatileDON'T use volatile
Simple boolean flags (stop, closed, cancelled)count++ (read-modify-write is NOT atomic)
One thread writes, others readMultiple threads writing to the same variable
Status indicatorsAnything needing check-then-act
volatile makes reads/writes visible, but does NOT make count++ atomic! count++ is read-then-add-then-write (3 steps). Two threads doing count++ on a volatile int can still lose increments. Use AtomicLong for counters (next section).

From your A4: volatile cancelRequested

final class JobRecord {
    volatile boolean cancelRequested = false;  // set by cancel thread
    JobStatus status = JobStatus.QUEUED;        // protected by synchronized (JobRegistry)
}

// In PrintWorker.run():
for (int p = 1; p <= job.pages(); p++) {
    if (r.cancelRequested) {      // volatile! Sees cancel immediately
        registry.markCancelled(jobId);
        return;
    }
    Thread.sleep(job.pageMillis());
}

Why volatile here instead of synchronized? The PrintWorker checks this flag in a tight loop. Using synchronized every iteration would be expensive. volatile gives cross-thread visibility without the overhead of a lock.

7. AtomicLong — The Turnstile Counter

You need a shared counter that multiple threads can safely increment. volatile isn't enough (count++ isn't atomic). synchronized works but is heavy. AtomicLong is the sweet spot: lock-free, thread-safe counters.

Memory Anchor: Stadium Turnstile
A turnstile at a cricket match. Hundreds of people push through simultaneously, but the counter never misses a person. That's an atomic counter — hardware-level magic (CAS: compare-and-swap) makes it work without locks.
import java.util.concurrent.atomic.AtomicLong;

private final AtomicLong idGen = new AtomicLong(1);
private final AtomicLong submitted = new AtomicLong(0);
private final AtomicLong completed = new AtomicLong(0);
private final AtomicLong cancelled = new AtomicLong(0);

// Thread-safe operations:
long id = idGen.getAndIncrement();    // returns old value, then adds 1
submitted.incrementAndGet();           // adds 1, then returns new value
long count = completed.get();          // just read the current value

When to use what

ScenarioToolWhy
Boolean flag (stop/cancel)volatile booleanSimple read/write, no arithmetic
Counter (submitted/completed)AtomicLongNeed atomic increment
Complex state (queue, map, state machine)synchronizedNeed multiple operations to happen as a unit

8. Producer-Consumer: BoundedBlockingQueue (from A4)

This is the while-wait + notifyAll pattern applied to a real problem. A queue with a maximum capacity. Producers wait if it's full. Consumers wait if it's empty.

Think of it like: a restaurant order counter with limited space for 5 order slips. When it's full, the waiter (producer) can't add more slips — they wait. When it's empty, the kitchen (consumer) has nothing to cook — they wait. When someone adds or removes a slip, they ring the bell (notifyAll).

The complete implementation

final class BoundedJobQueue {
    private final int capacity;
    private final Deque<Long> q = new ArrayDeque<>();
    private int maxDepth = 0;

    BoundedJobQueue(int capacity) { this.capacity = capacity; }

    /* === PRODUCER: blocks until space available === */
    synchronized void putBlocking(long jobId) throws InterruptedException {
        while (q.size() >= capacity)    // full? wait.
            wait();                       // (Iron Rule #1: while, not if)
        q.addLast(jobId);
        if (q.size() > maxDepth) maxDepth = q.size();
        notifyAll();                      // someone might be waiting to take
    }

    /* === CONSUMER: blocks until item available === */
    synchronized long takeBlocking() throws InterruptedException {
        while (q.isEmpty())              // empty? wait.
            wait();
        long id = q.pollFirst();
        notifyAll();                      // someone might be waiting to put
        return id;
    }

    /* === PRODUCER WITH TIMEOUT === */
    synchronized boolean putWithTimeout(long jobId, long timeoutMs)
            throws InterruptedException {
        long deadline = System.currentTimeMillis() + timeoutMs;
        while (q.size() >= capacity) {
            long remaining = deadline - System.currentTimeMillis();
            if (remaining <= 0) return false;  // timed out!
            wait(remaining);                     // wait with timeout
        }
        q.addLast(jobId);
        if (q.size() > maxDepth) maxDepth = q.size();
        notifyAll();
        return true;
    }

    /* === CANCEL: remove a specific job if still queued === */
    synchronized boolean removeIfPresent(long jobId) {
        boolean removed = q.remove(jobId);
        if (removed) notifyAll();          // freed a slot!
        return removed;
    }
}
Mental Trace: Capacity = 2
State: queue = [job1, job2] (FULL)

Producer thread: calls putBlocking(job3)
  Grabs lock. q.size() (2) >= capacity (2)? Yes.
  Calls wait(). Releases lock. Sleeps.

Consumer thread: calls takeBlocking()
  Grabs lock (producer released it). q.isEmpty()? No.
  Takes job1. Calls notifyAll(). Releases lock.

Producer wakes up:
  Re-acquires lock. Back to while: q.size() (1) >= 2? No!
  Adds job3. Calls notifyAll(). Done.

The timeout pattern

putWithTimeout is the same pattern but with a deadline. wait(remaining) wakes up either when notified OR when time runs out. After waking, check if the condition changed AND if you've run out of time.

Memory Anchor: Timeout = "I'll wait, but I have a train to catch"
"I'll wait at the restaurant for a table, but only 15 minutes. If no table opens by then, I'm leaving." Compute a deadline, keep checking remaining time, leave (return false) when it hits zero.

9. State Machines — Guarded Transitions (from A4: JobRegistry)

A job in the print spooler has a lifecycle. It starts QUEUED, moves to PRINTING, then to DONE or CANCELLED. You can't go backwards. You can't skip steps. This is a state machine.

/*  The valid transitions:
 *
 *  QUEUED  ──────> PRINTING ──────> DONE
 *     │                │
 *     └── CANCELLED <──┘
 *
 *  DONE and CANCELLED are terminal. No transitions from them.
 */
Think of it like: a traffic light. It goes GREEN → YELLOW → RED in order. You can't go from GREEN straight to RED. Each transition has a guard: "Only change to YELLOW if currently GREEN." In code, that guard is an if check on the current state.

The implementation

final class JobRegistry {
    private final Map<Long, JobRecord> jobs = new HashMap<>();

    // Register a new job
    synchronized JobRecord create(long jobId, PrintJob job) {
        JobRecord r = new JobRecord(jobId, job);
        jobs.put(jobId, r);
        return r;
    }

    // QUEUED → PRINTING (but NOT if cancel was requested!)
    synchronized boolean markPrinting(long jobId) {
        JobRecord r = jobs.get(jobId);
        if (r == null) return false;
        if (r.status != JobStatus.QUEUED) return false;     // guard
        if (r.cancelRequested) {                            // RACE CHECK!
            r.status = JobStatus.CANCELLED;
            return false;
        }
        r.status = JobStatus.PRINTING;
        return true;
    }

    // PRINTING → DONE
    synchronized void markDone(long jobId) {
        JobRecord r = jobs.get(jobId);
        if (r != null && r.status == JobStatus.PRINTING)    // guard
            r.status = JobStatus.DONE;
    }

    // QUEUED|PRINTING → CANCELLED (but not from DONE)
    synchronized boolean markCancelled(long jobId) {
        JobRecord r = jobs.get(jobId);
        if (r == null) return false;
        if (r.status == JobStatus.DONE) return false;       // can't cancel done
        r.status = JobStatus.CANCELLED;
        r.cancelRequested = true;                           // volatile flag for worker
        return true;
    }

    synchronized JobStatus status(long jobId) {
        JobRecord r = jobs.get(jobId);
        return r == null ? null : r.status;
    }
}

The cancel-vs-start race

This is the trickiest part. What if Thread A calls cancel() at the EXACT same time Thread B calls markPrinting()?

Mental Trace: The Race
Thread A (cancel): enters markCancelled, grabs lock first.
  Sets cancelRequested = true (volatile, visible immediately)
  Sets status = CANCELLED
  Releases lock.

Thread B (dispatcher): enters markPrinting, grabs lock.
  Checks: r.status != QUEUED? It's CANCELLED now → return false.
  Job never starts printing. Correct!

What if Thread B got the lock first?
  Checks: r.cancelRequested? It's already true (volatile!) → sets CANCELLED, return false.
  Still correct! The volatile flag is the safety net.
State Machine Recipe
  • Every transition method is synchronized
  • First line: get the record. Check null.
  • Guard: check current state. Wrong state? Return false.
  • Set new state. Return true.
  • For cancel races: check cancelRequested (volatile) inside markPrinting

10. Thread Pools & ExecutorService

Creating a new Thread for every task is expensive (OS has to allocate a stack, schedule it, etc.). A thread pool pre-creates a fixed number of worker threads that pick up tasks from a queue.

Think of it like: a restaurant with 4 cooks. When an order comes in, the next free cook takes it. You don't hire and fire a new cook for every dish — that would be insane. The pool of 4 cooks is your ExecutorService.
import java.util.concurrent.*;

// Create a pool of 4 worker threads
ExecutorService pool = Executors.newFixedThreadPool(4);

// Submit work (returns immediately; work runs in background)
pool.submit(() -> {
    System.out.println("Running on: " + Thread.currentThread().getName());
});

// Submit more work
pool.submit(() -> doHeavyComputation());

The graceful shutdown sequence (critical for exam!)

// Step 1: Stop accepting new tasks
pool.shutdown();

// Step 2: Wait for current tasks to finish (with timeout)
if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
    // Step 3: Timed out? Force-kill remaining tasks
    pool.shutdownNow();
}
Memory Anchor: Closing a Restaurant
shutdown() = "Kitchen is closed. No new orders. But finish what's on the stove."
awaitTermination(5s) = "I'll wait 5 minutes for you to finish cooking."
shutdownNow() = "TIME'S UP. Turn off the stoves. Everyone out."

shutdown() is polite. shutdownNow() interrupts all running threads (sends them an InterruptedException). You almost always want to try shutdown() first, then fall back to shutdownNow().

11. The Print Spooler — All of A4 in One Picture

Your assignment is a complete print spooler system. Here's how all the pieces fit together:

/*
 *  [User Thread]                    [Dispatcher Thread]              [Worker Pool]
 *       |                                 |                              |
 *  submitBlocking(job)                    |                              |
 *       |                                 |                              |
 *  +--> registry.create(id, job)          |                              |
 *  +--> queue.putBlocking(id) ----+       |                              |
 *       |                         |       |                              |
 *       |                    BoundedJobQueue                             |
 *       |                    [id1, id2, ...]                             |
 *       |                         |       |                              |
 *       |                         +-----> queue.takeBlocking() -----+    |
 *       |                                 |                         |    |
 *       |                                 +--> pool.submit(         |    |
 *       |                                 |     PrintWorker(id))    |    |
 *       |                                 |                         |    |
 *       |                                 |                    [PrintWorker]
 *       |                                 |                    markPrinting(id)
 *       |                                 |                    for each page:
 *       |                                 |                      if cancelRequested: bail
 *       |                                 |                      sleep(pageMillis)
 *       |                                 |                    markDone(id)
 *       |                                 |                         |
 *  cancel(id) -----------------------> markCancelled(id)            |
 *       |                              sets cancelRequested=true    |
 *       |                              removes from queue if still  |
 *       |                              queued                       |
 */

SpoolerImpl — the coordinator

public final class SpoolerImpl implements Spooler {
    private final AtomicLong idGen = new AtomicLong(1);      // unique job IDs
    private final AtomicLong submitted = new AtomicLong(0);  // metrics counters
    private final AtomicLong completed = new AtomicLong(0);
    private final AtomicLong cancelled = new AtomicLong(0);

    private final JobRegistry registry = new JobRegistry();
    private final BoundedJobQueue queue;
    private final ExecutorService pool;
    private final Thread dispatcher;
    private volatile boolean closed = false;                 // shutdown flag

    public SpoolerImpl(int capacity, int workers) {
        this.queue = new BoundedJobQueue(capacity);
        this.pool = Executors.newFixedThreadPool(workers);

        // Dispatcher: takes jobs from queue, submits to pool
        this.dispatcher = new Thread(() -> {
            while (!closed) {
                try {
                    long jobId = queue.takeBlocking();
                    pool.submit(() -> {
                        new PrintWorker(jobId, registry).run();
                        JobStatus st = registry.status(jobId);
                        if (st == JobStatus.DONE) completed.incrementAndGet();
                        else if (st == JobStatus.CANCELLED) cancelled.incrementAndGet();
                    });
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }, "spooler-dispatcher");
        dispatcher.start();
    }

    // Submit a job (blocks if queue is full)
    public long submitBlocking(PrintJob job) throws InterruptedException {
        if (closed) throw new IllegalStateException("spooler closed");
        long id = idGen.getAndIncrement();
        registry.create(id, job);
        queue.putBlocking(id);
        submitted.incrementAndGet();
        return id;
    }

    // Cancel a job
    public boolean cancel(long jobId) {
        boolean ok = registry.markCancelled(jobId);
        if (ok) {
            queue.removeIfPresent(jobId);      // remove from queue if still there
            cancelled.incrementAndGet();
        }
        return ok;
    }

    // Graceful shutdown
    public void close() throws Exception {
        closed = true;                        // volatile: dispatcher sees it
        dispatcher.interrupt();                // wake from takeBlocking
        dispatcher.join(5000);                 // wait up to 5s
        pool.shutdown();
        if (!pool.awaitTermination(5, TimeUnit.SECONDS))
            pool.shutdownNow();
    }
}

PrintWorker — the actual "printer"

final class PrintWorker implements Runnable {
    private final long jobId;
    private final JobRegistry registry;

    public void run() {
        JobRecord r = registry.get(jobId);
        if (r == null) return;

        if (!registry.markPrinting(jobId)) return;   // cancelled before start

        PrintJob job = r.job;
        for (int p = 1; p <= job.pages(); p++) {
            if (r.cancelRequested) {                    // volatile check each page
                registry.markCancelled(jobId);
                return;
            }
            try {
                Thread.sleep(job.pageMillis());              // simulate printing
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                registry.markCancelled(jobId);
                return;
            }
        }
        registry.markDone(jobId);                             // all pages printed!
    }
}
Architecture Summary: Who Uses What
  • AtomicLong: ID generator + metrics counters (fast, lock-free)
  • synchronized + wait/notifyAll: BoundedJobQueue (blocking producer-consumer)
  • synchronized: JobRegistry (state machine transitions)
  • volatile boolean: closed flag (shutdown), cancelRequested (cancel mid-print)
  • ExecutorService: worker thread pool (reuse threads)

12. InterruptedException — The Polite Tap on the Shoulder

Thread.interrupt() does NOT kill a thread. It sets a flag and wakes the thread up from any blocking call (wait(), sleep(), join()). The thread then has to CHOOSE to stop.

Memory Anchor: The Polite Tap
Someone taps you on the shoulder while you're sleeping in a meeting. They can't physically drag you out — they just make you aware that maybe you should leave. You get to decide: leave now, or ignore it and go back to sleep. interrupt() is the tap.

The standard catch pattern

try {
    Thread.sleep(1000);         // or wait(), or join(), or queue.take()
} catch (InterruptedException e) {
    // IMPORTANT: sleep/wait CLEARS the interrupt flag when they throw!
    // Re-set it so callers above us know we were interrupted:
    Thread.currentThread().interrupt();
    return;                     // or break, or handle gracefully
}
Why restore the flag? When sleep() or wait() throws InterruptedException, they clear the interrupt flag. If you catch the exception and don't restore the flag, code higher up the call stack won't know the thread was interrupted. Always call Thread.currentThread().interrupt() in the catch block.
Memory Anchor: The Sticky Note
Someone put a sticky note on your monitor: "Please stop." You see it, pull it off to read it, but now there's no sticky note for your boss to see. Thread.currentThread().interrupt() = putting a new sticky note back so the message isn't lost.

Two strategies for InterruptedException

StrategyWhen to useCode
Propagate Method can throw it Just add throws InterruptedException to the signature
Catch + restore Inside run() or can't throw catch (IE e) { Thread.currentThread().interrupt(); return; }

13. DOMjudge I/O Template for Java

// CRITICAL: NO package declaration! Class MUST be named Main.
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        // Read a single int
        int n = sc.nextInt();

        // Read n ints into an array
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) arr[i] = sc.nextInt();

        // Read a whole line (careful: nextLine after nextInt needs an extra nextLine)
        sc.nextLine();  // consume the leftover newline
        String line = sc.nextLine();

        // Read until EOF
        while (sc.hasNextInt()) {
            int x = sc.nextInt();
        }

        // Output
        System.out.println(result);       // with newline
        System.out.print(result);         // without newline
    }
}
DOMjudge gotchas:
1. NO package line at the top. DOMjudge compiles your file directly.
2. Class MUST be public class Main.
3. nextInt() leaves a \n in the buffer. If you call nextLine() after, it reads that empty \n. Add an extra sc.nextLine() to clear it.

14. Practice Problems

Problem 1: Thread-Safe Bounded Counter

Implement a BoundedCounter with methods increment() (blocks at max) and decrement() (blocks at 0). (Uses: synchronized, while-wait, notifyAll — sections 4, 5)

Show Solution
class BoundedCounter {
    private int count = 0;
    private final int max;

    BoundedCounter(int max) { this.max = max; }

    synchronized void increment() throws InterruptedException {
        while (count >= max) wait();   // full? wait.
        count++;
        notifyAll();                    // someone might be waiting to decrement
    }

    synchronized void decrement() throws InterruptedException {
        while (count <= 0) wait();     // empty? wait.
        count--;
        notifyAll();                    // someone might be waiting to increment
    }

    synchronized int getCount() { return count; }
}

Exact same structure as BoundedJobQueue. Replace "queue full/empty" with "counter at max/zero."

Problem 2: Thread-Safe State Tracker

Implement StateTracker with states: IDLE → RUNNING → FINISHED, also IDLE|RUNNING → CANCELLED. FINISHED and CANCELLED are terminal. (Uses: synchronized, state machine guards — sections 4, 9)

Show Solution
enum State { IDLE, RUNNING, FINISHED, CANCELLED }

class StateTracker {
    private State state = State.IDLE;

    synchronized boolean start() {
        if (state != State.IDLE) return false;
        state = State.RUNNING;
        return true;
    }

    synchronized boolean finish() {
        if (state != State.RUNNING) return false;
        state = State.FINISHED;
        return true;
    }

    synchronized boolean cancel() {
        if (state == State.FINISHED || state == State.CANCELLED)
            return false;
        state = State.CANCELLED;
        return true;
    }

    synchronized State getState() { return state; }
}

Same pattern as JobRegistry. Guard check → transition → return success.

Problem 3: Producer-Consumer with Poison Pill

Implement a MessageQueue (unbounded) where the producer sends strings. The special message "QUIT" signals the consumer to stop. (Uses: synchronized, while-wait, notifyAll — section 5)

Show Solution
class MessageQueue {
    private final Queue<String> q = new LinkedList<>();

    synchronized void put(String msg) {
        q.add(msg);
        notifyAll();
    }

    synchronized String take() throws InterruptedException {
        while (q.isEmpty()) wait();
        return q.poll();
    }
}

// Consumer loop:
// while (true) {
//     String msg = queue.take();
//     if ("QUIT".equals(msg)) break;
//     process(msg);
// }

The poison pill pattern: a special sentinel value that tells the consumer to shut down. Simpler than interrupting.

Problem 4: Implement close() for the Spooler

Given SpoolerImpl with a dispatcher thread, pool, and volatile boolean closed, write the close() method that gracefully shuts everything down. (Uses: volatile, interrupt, join, shutdown sequence — sections 6, 10, 12)

Show Solution
public void close() throws Exception {
    closed = true;                          // 1. Signal dispatcher to stop
    dispatcher.interrupt();                  // 2. Wake from takeBlocking
    dispatcher.join(5000);                   // 3. Wait for dispatcher to exit
    pool.shutdown();                         // 4. No new tasks to pool
    if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
        pool.shutdownNow();                  // 5. Force if still running
    }
}

Order matters! Stop the dispatcher first (so it stops submitting), then shut down the pool. Setting closed = true (volatile) makes the dispatcher loop see it immediately. interrupt() wakes it from the blocking takeBlocking() call.