Project Based Learning in Java

Thread Control and Coordination

sleep, join, interruption, thread metadata, safe cancellation, and common mistakes.

Lecture 2.1.3 CO3 aligned Unit 2 · Chapter 2.1

Learning outcome: Coordinate threads with sleep, join, and interruption; inspect thread metadata; and avoid unsafe or deprecated thread-control techniques.

Starting threads is only the first step. Real programs also need to wait for work, pause between actions, request cancellation, and know when a worker has finished. Java provides cooperative tools for these tasks.

sleep(): Pause the Current Thread

Thread.sleep temporarily pauses the thread that calls it.

In simple terms: Thread.sleep always puts the thread that executes that line to sleep. Writing worker.sleep(500) compiles, but it does not pause worker; it pauses whoever ran the statement. It is a static method, so always write Thread.sleep(...).

public class Countdown {
    public static void main(String[] args) {
        for (int value = 3; value >= 1; value--) {
            System.out.println(value);
            try {
                Thread.sleep(500);
            } catch (InterruptedException exception) {
                Thread.currentThread().interrupt();
                System.out.println("Countdown interrupted");
                return;
            }
        }
        System.out.println("Go!");
    }
}

Output:

3
2
1
Go!

Note the timing: the three numbers appear about half a second apart, and Go! follows the last pause. sleep(500) promises "at least 500 milliseconds", never "exactly 500".

Important points:

  • sleep is a static method and affects the current thread.
  • The requested duration is a minimum scheduling delay, not an exact wake-up time.
  • A sleeping thread enters TIMED_WAITING.
  • Interruption causes InterruptedException.
  • Sleeping does not release monitor locks already held by the thread.

Sleep keeps the key: a sleeping thread still holds every monitor it has acquired, so other threads remain BLOCKED for the whole duration. Avoid sleeping inside a synchronized block unless there is a specific reason.

join(): Wait for Completion

join makes the current thread wait for another thread to terminate.

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            System.out.println("Worker started");
            try {
                Thread.sleep(300);
            } catch (InterruptedException exception) {
                Thread.currentThread().interrupt();
                return;
            }
            System.out.println("Worker finished");
        });

        worker.start();
        worker.join();
        System.out.println("Main continues");
    }
}

Output:

Worker started
Worker finished
Main continues

In simple terms: worker.join() reads as "wait here until worker has finished". The waiting is done by the thread that calls join, normally main, not by the worker itself.

Without join, the final two lines could appear in either order. Timed forms such as join(1000) wait at most the specified duration.

wait(), notify(), and notifyAll()

sleep waits for time to pass and join waits for a thread to finish. When a thread must wait for a condition that another thread will make true, the monitor itself provides the mechanism: wait, notify, and notifyAll, which are declared on Object rather than on Thread.

Three rules follow from the API specification:

  1. The calling thread must already own that object's monitor. Calling any of the three outside a synchronized region throws IllegalMonitorStateException.
  2. wait() releases the monitor while it waits and re-acquires it before returning. This is the essential difference from sleep.
  3. A thread may wake without being notified, interrupted, or timed out. This is a spurious wakeup, so the condition must be re-tested after waking.

In simple terms: wait() states that the thread cannot proceed yet and gives the monitor back so that another thread can make the condition true. sleep() keeps the monitor. That is why a wait inside a synchronized block allows another thread to enter, and a sleep in the same place does not.

Rule 3 is why the documentation places the call inside a while loop rather than an if:

synchronized (shared) {
    while (!conditionHolds()) {
        shared.wait();
    }
    // the condition now holds and this thread owns the monitor
}

Producer and Consumer

Two threads exchange values through one shared slot. The producer waits while the slot is full; the consumer waits while it is empty.

class MessageBox {
    private String message;
    private boolean available = false;

    synchronized void put(String value) throws InterruptedException {
        while (available) {          // while, not if: guards against spurious wakeup
            wait();
        }
        message = value;
        available = true;
        System.out.println("Produced: " + value);
        notifyAll();
    }

    synchronized String take() throws InterruptedException {
        while (!available) {
            wait();
        }
        available = false;
        System.out.println("Consumed: " + message);
        notifyAll();
        return message;
    }
}

Output:

Produced: item-1
Consumed: item-1
Produced: item-2
Consumed: item-2
Produced: item-3
Consumed: item-3
Exchange complete

notify() wakes one arbitrarily chosen waiting thread; notifyAll() wakes every waiting thread, each of which then re-tests its own condition. Prefer notifyAll() unless it is certain that all waiting threads are waiting for the same condition.

sleep() versus wait()

Question Thread.sleep(ms) object.wait()
Declared on Thread, and static Object, and an instance method
Must the caller hold a monitor? No Yes, the monitor of that object
Releases the monitor No Yes
Wakes when The duration expires Notified, interrupted, timed out, or spuriously
Needs another thread No Yes, to call notify or notifyAll
Typical state TIMED_WAITING WAITING, or TIMED_WAITING for wait(ms)

For production code, the higher-level types in java.util.concurrent, such as BlockingQueue, implement this exchange already and should be preferred over hand-written wait/notify.

Interruption Is a Cooperative Request

interrupt() does not forcibly kill a thread. It sends a request that well-designed tasks detect and handle.

In simple terms: interruption is a tap on the shoulder, not a power switch. It sets a flag that means "please wrap up". A task that never checks the flag and never calls a blocking method simply keeps running, so cancellation works only when the task cooperates.

Two situations are common:

  1. A thread blocked in sleep, join, or wait receives InterruptedException.
  2. A thread doing ordinary work can check its interrupt status with isInterrupted().
public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println("Working...");
                    Thread.sleep(200);
                } catch (InterruptedException exception) {
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("Worker stopped safely");
        }, "safe-worker");

        worker.start();
        Thread.sleep(550);
        worker.interrupt();
        worker.join();
    }
}

Typical output:

Working...
Working...
Working...
Worker stopped safely

The catch block restores the interrupt status because throwing InterruptedException clears it. The loop then observes the restored flag and exits in an orderly way.

Why re-interrupt inside catch? Delivering InterruptedException wipes the flag clean. If the catch block did nothing, the loop condition would see "not interrupted" and spin forever. Calling Thread.currentThread().interrupt() puts the message back so the loop can act on it.

isInterrupted() vs interrupted()

Method Checks Clears the flag?
thread.isInterrupted() The specified thread No
Thread.interrupted() The current thread Yes

Use isInterrupted() for most loop conditions. Use the clearing behaviour of Thread.interrupted() only when the design intentionally consumes the request.

In simple terms: isInterrupted() reads the note and leaves it on the desk. Thread.interrupted() reads the note and tears it up, so the very next call returns false.

Thread Names and Status Methods

Names make logs and debugging easier.

public class ThreadInfo {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(
                () -> System.out.println("Task: " + Thread.currentThread().getName()),
                "report-worker");

        System.out.println("Before start: " + worker.getState());
        worker.start();
        System.out.println("Alive after start: " + worker.isAlive());
        worker.join();
        System.out.println("After join: " + worker.getState());
    }
}

Sample output (the middle two lines may swap, because both threads print at once):

Before start: NEW
Task: report-worker
Alive after start: true
After join: TERMINATED

Useful methods include:

Method Purpose
getName() Returns the thread name
setName(name) Changes the thread name
getState() Returns a monitoring snapshot of the JVM state
isAlive() Tests whether the thread has started and not terminated
currentThread() Returns the currently executing Thread object
threadId() Returns the thread identifier on JDK 19 and later

For the JDK 17 course baseline, use getId(). It is deprecated in newer JDKs, where threadId() is preferred.

A Thread Cannot Be Restarted

Once start() has been invoked, the same Thread object cannot be started again, even after it terminates.

Thread worker = new Thread(() -> System.out.println("Running"));
worker.start();
worker.join();
// worker.start(); // IllegalThreadStateException

Create a new Thread object when the task must run again.

In simple terms: a Thread object is a matchstick, not a lighter: it lights once. The Runnable, however, is reusable, so hand the same task object to a fresh Thread whenever it must run again.

Avoid Unsafe Thread Control

Older notes may list stop, suspend, and resume. All three are deprecated for removal because they can leave shared objects locked or inconsistent. On JDK 20 and later they no longer do anything at all: each one throws UnsupportedOperationException when called. Do not use them.

Unsafe idea Safe direction
Force a worker to stop Request cancellation with interrupt or a controlled flag
Suspend a thread while it may hold locks Coordinate through higher-level concurrency tools
Depend on thread priority for correctness Express ordering explicitly with coordination
Call yield to guarantee fairness Treat yield only as a scheduler hint; it may be ignored

Thread coordination should be explicit and cooperative.

sleep() vs join()

Question sleep join
Which thread waits? The current thread The current thread
Waits for what? Time to pass Another thread to terminate
Typical state TIMED_WAITING WAITING or TIMED_WAITING
Main purpose Delay or pacing Completion dependency
Responds to interruption Yes Yes

Do not use an arbitrary sleep as a substitute for waiting for another thread's completion. Use join when completion is the actual condition.

Common Mistakes

Mistake Correction
Calling worker.sleep(500) and assuming it sleeps worker sleep is static and sleeps the current thread. Call Thread.sleep.
Ignoring InterruptedException Restore the flag or exit according to the task's cancellation policy.
Using sleep to guess when a worker is done Use join.
Calling start twice on one thread object Create a new Thread.
Using stop, suspend, or resume Use cooperative cancellation and safe concurrency utilities.
Using getState for synchronisation State is a monitoring snapshot, not a coordination guarantee.
Calling wait, notify, or notifyAll outside a synchronized region All three need the object's monitor; otherwise IllegalMonitorStateException is thrown.
Testing a wait condition with if instead of while A spurious wakeup can resume the thread while the condition is still false.
Expecting wait to keep the monitor, or sleep to release it wait releases the monitor; sleep retains it.

Practice

  1. Create a worker that prints five messages 250 milliseconds apart.
  2. Start two workers and use join so main prints a final total only after both finish.
  3. Build a loop that exits when its thread is interrupted.
  4. Print a worker's name, alive status, and state before and after execution.
  5. Implement the producer and consumer example and explain why take uses while rather than if.
  6. Show that a thread in wait() allows another thread to enter the same synchronized method, while a thread in sleep() does not.

Quick Check

1. Which thread does Thread.sleep pause?

It pauses the currently executing thread.

2. What does join() wait for?

It waits for the target thread to terminate.

3. Does interrupt() forcibly terminate a thread?

No. Interruption is a cooperative cancellation request that the target task must handle.

4. Can the same Thread object be started twice?

No. A thread can be started at most once.

5. Does wait() release the monitor?

Yes. wait() releases the monitor of the object it is called on and re-acquires it before returning. Thread.sleep does not.

6. Why is wait() called inside a while loop?

Because a thread can wake spuriously, without any notification. The loop re-tests the condition, so the thread continues only when the condition genuinely holds.

7. On which class are wait and notify declared?

On Object, because the mechanism belongs to the monitor of an object rather than to a thread.

Lecture Quiz

A worker is sleeping when another thread interrupts it. What happens?

The sleeping call ends by throwing InterruptedException, and the interrupt status is cleared as the exception is delivered.

Summary

sleep pauses the current thread for a duration, while join waits for another thread to finish. wait, notify, and notifyAll coordinate on a condition through an object's monitor; wait releases that monitor and must be re-tested in a loop because of spurious wakeups. Interruption is Java's cooperative cancellation mechanism and should be handled deliberately. Thread names and status methods aid monitoring, but correctness should rely on explicit coordination rather than timing guesses or deprecated force-control methods.