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:
sleepis 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:
- The calling thread must already own that object's monitor. Calling any of the three outside a
synchronizedregion throwsIllegalMonitorStateException. wait()releases the monitor while it waits and re-acquires it before returning. This is the essential difference fromsleep.- 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:
- A thread blocked in
sleep,join, orwaitreceivesInterruptedException. - 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
- Create a worker that prints five messages 250 milliseconds apart.
- Start two workers and use
joinso main prints a final total only after both finish. - Build a loop that exits when its thread is interrupted.
- Print a worker's name, alive status, and state before and after execution.
- Implement the producer and consumer example and explain why
takeuseswhilerather thanif. - Show that a thread in
wait()allows another thread to enter the same synchronized method, while a thread insleep()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.