🧵 Concurrency Fundamentals · Advanced

sleep, join & interrupt in Java

Cooperative cancellation, InterruptedException, restoring the interrupt flag.

🧩 The mysteryYou need a worker thread to stop. Right now. Java gives you no kill switch, only a polite tap on the shoulder. Here's why that's a feature, not a flaw.

Interrupt is a polite note

t.interrupt() only sets a flag on t. It never forcibly stops it. A thread busy calculating keeps going until it checks the flag itself. This is cooperative cancellation: the thread decides where it's safe to stop.

Thread me = Thread.currentThread();
while (!me.isInterrupted()) {
    crunchNextChunk();
}
cleanUp();

Blocking methods react

sleep(), join() and wait() respond to an interrupt by throwing InterruptedException, immediately, even if the interrupt arrived *before* the call. And when they throw, they clear the flag. The exception now carries the request; the flag no longer does.

🔮 Predict it

Your turn

main interrupts itself, then sleeps. What does this print?

Thread.currentThread().interrupt();
try {
    Thread.sleep(5000);
    System.out.println("woke up");
} catch (InterruptedException e) {
    System.out.println("caught");
    Thread.currentThread().interrupt();
}
System.out.println(
    Thread.currentThread().isInterrupted());
  1. caught true
  2. caught false
  3. woke up true
Show the answer

sleep() sees the pending interrupt and throws at once, which clears the flag. But the catch block restores it with interrupt(), so the last line prints true. Delete that line and it would print false.

Handling InterruptedException

✗ Swallowed
} catch (InterruptedException e) {
    log.warn("ignored");
}

The flag was cleared and now the request is lost. A while (!isInterrupted()) loop never sees it, and pool shutdown hangs.

✓ Restored
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
}

Restores the flag so code higher up the stack, like a thread pool shutting down, still sees the request.

⚠️ The trap

interrupted() vs isInterrupted()

t.isInterrupted() just reads the flag. The static Thread.interrupted() reads and clears the current thread's flag. So calling Thread.interrupted() in a catch block does the opposite of restoring: it wipes the request. To restore, call Thread.currentThread().interrupt().

join(): wait for a finisher

t.join() blocks the caller until thread t has finished. It also guarantees the caller then sees everything t wrote. So here "B" is always appended after "A", and main prints AB.

StringBuilder sb = new StringBuilder();
Thread t = new Thread(() -> sb.append("A"));
t.start();
t.join();        // wait until t is done
sb.append("B");  // always after "A"
System.out.println(sb);
💼 In the real world

Why your app won't shut down

ExecutorService.shutdownNow() cancels running tasks by interrupting their threads. A task that swallows InterruptedException ignores the request, so shutdown hangs. In containers this is a classic: the pod ignores the stop signal until the platform's grace period runs out and it gets killed hard.

Key takeaways

  1. interrupt() sets a flag; it never forcibly stops a thread
  2. sleep/join/wait throw InterruptedException and clear the flag
  3. Restore the flag if you swallow the exception
  4. t.join() waits until thread t has finished
🤯 Did you know?

Java 1.0 had Thread.stop(). It could leave objects half-updated, was deprecated in 1998, and since Java 20 it simply throws UnsupportedOperationException.

Practice questions

What does this print?

Thread.currentThread().interrupt();
try {
    Thread.sleep(1000);
    System.out.println("slept");
} catch (InterruptedException e) {
    System.out.println("interrupted");
}
System.out.println(
    Thread.currentThread().isInterrupted());
  1. interrupted false
  2. interrupted true
  3. slept true
  4. slept false
Check your answer

interrupted false. sleep() sees the pending interrupt and throws immediately. Throwing InterruptedException clears the flag, so isInterrupted() then returns false.

What does this print?

StringBuilder sb = new StringBuilder();
Thread t = new Thread(() -> sb.append("A"));
t.start();
t.join();
sb.append("B");
System.out.println(sb);
  1. AB
  2. BA
  3. A
  4. B
Check your answer

AB. join() blocks main until the worker has finished appending "A", so "B" always comes second. join() also makes the worker's write visible to main.

Next: two threads each add 1 to a counter a million times. The total comes out short. Where did the increments go?