14.1 Thread Creation and Lifecycle
After learning about methods, objects, Lambdas, and exceptions, you'll spend about 50 minutes running these Java 17 platform-thread experiments. You'll be able to start, wait for, and cooperatively cancel threads, and explain what each of the six
Thread.Statevalues represents.
One Person Handles Two Windows
This is a good place to represent the two tasks as independent jobs. Don't rush to discuss shared inventory; this page only answers: how to assign tasks to threads, how callers know when threads complete, and how programs request threads to stop.
1. Runnable is a task, Thread is the executor
Runnable printReceipt = () ->
System.out.println("Print receipt:" + Thread.currentThread().getName());
Thread worker = new Thread(printReceipt, "receipt-worker");
worker.start();The single abstract method of Runnable is void run(). It only describes work and does not own a thread.
You can also inherit Thread and override run():
final class CatalogThread extends Thread {
CatalogThread() {
super("catalog-worker");
}
@Override
public void run() {
System.out.println("Query catalog");
}
}This approach works, but it binds the task and thread lifecycle together, also occupying the single inheritance slot in Java. Usually, give a Runnable to Thread. When a Callable must return a result, use an executor, covered later in this chapter.
2. run() and start() are two different things
Calling run() is just a regular method call, still executed by the current thread. Calling start() requests the JVM to establish a new execution path, and run() is called by the new thread.
Here is a complete, stable experiment:
public class ThreadLifecycleDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(
() -> System.out.println("Task thread: " + Thread.currentThread().getName()),
"library-worker");
worker.run();
worker.start();
worker.join();
if (worker.isAlive() || worker.getState() != Thread.State.TERMINATED) {
throw new AssertionError("The thread should have terminated after join returns");
}
System.out.println("Thread check passed");
}
}Run:
chapter14_thread_root=$(mktemp -d)
mkdir -p "$chapter14_thread_root/out"
# Save the above code as "$chapter14_thread_root/ThreadLifecycleDemo.java"
javac --release 17 -Xlint:all -Werror \
-d "$chapter14_thread_root/out" \
"$chapter14_thread_root/ThreadLifecycleDemo.java"
java -ea -cp "$chapter14_thread_root/out" ThreadLifecycleDemo
rm -rf -- "$chapter14_thread_root"Output:
Task thread: main
Task thread: library-worker
Thread check passedThe experiment first calls worker.run() directly, then starts the worker thread, and finally uses join() to wait for it to end, so the order of the three lines is determined. There is usually no such order guarantee between multiple already started threads.
3. A Thread Can Only Be Started Once
Once a thread has moved from the unstarted state to the terminated state, it cannot go back:
Thread worker = new Thread(() -> { });
worker.start();
worker.join();
worker.start(); // IllegalThreadStateExceptionExceptions are not due to the failure of the first task, but because the lifecycle of the thread object is one-time. Repeated work should create new threads or reuse work threads from a thread pool, rather than reusing the Thread object.
4. Six Java Thread States
NEW --start()--> RUNNABLE -----------------------> TERMINATED
| ^
| | Acquire monitor/condition satisfied/time-out
v |
BLOCKED / WAITING / TIMED_WAITING| State | Typical Reason | Key Difference |
|---|---|---|
NEW | After new Thread(...) and before start() | Not yet started |
RUNNABLE | Runnable or executing | Java has no separate RUNNING state; some OS-resource waits also appear here |
BLOCKED | Waiting to enter synchronized region | Waiting for a monitor lock |
WAITING | wait(), join(), park() without timeout | Waiting for another action, no deadline |
TIMED_WAITING | sleep(), timed wait()/join() | Has a deadline |
TERMINATED | run() returns or exits due to an uncaught exception | Cannot be restarted again |
Thread State Snapshots Cannot Be Used for Coordination
Thread.State observed = worker.getState();The line read is a transient state. Before the next line executes, the thread may have switched states. Therefore, do not write protocols like "wait for getState() == WAITING before modifying data" or use fixed millisecond counts of sleep() to guess where another thread has progressed.
When you need to determine the sequence, use join(), CountDownLatch, BlockingQueue, Phaser, or other explicit collaboration tools. Thread state snapshots are suitable for logging, monitoring, and thread dump analysis.
5. join() Establishing Completion Relationships
final int[] result = new int[1];
Thread worker = new Thread(() -> result[0] = 42);
worker.start();
worker.join();
System.out.println(result[0]);Here, the worker has been started and the untimed join() confirms its termination. The worker’s actions happen-before the caller’s subsequent actions. This allows us to see 42. If we remove join(), the main thread doesn't know when the assignment occurs and shouldn't rely directly on the result.
Calling join() before a thread has started returns immediately; it does not replace waiting after startup. join() can be interrupted. If the caller doesn't want to wait indefinitely, they can use join(timeout), but after the timeout returns, they must check worker.isAlive() again; a timeout does not mean the thread has finished.
6. Naming, Daemon Threads, and Uncaught Exceptions
Naming threads can directly improve logging and thread dumps:
Thread worker = new Thread(task, "library-catalog-1");
worker.setUncaughtExceptionHandler((thread, error) ->
System.err.println(thread.getName() + " Failure:" + error));Threads are divided into user threads and daemon threads. When the JVM is left only with daemon threads, it can exit without waiting for the daemon threads to complete.
worker.setDaemon(true); // Call before start()Daemon threads are not a reliable mechanism for resource cleanup: the JVM may not have time to flush buffers or complete writes when it exits. Data that must be completed should be managed with explicit lifecycles, close signals, and join()/executor shutdown.
Uncaught exceptions terminate the current thread. Ordinary Thread can set UncaughtExceptionHandler; in a thread pool, the exception path differs between execute() and submit(), as detailed in the thread pool topic.
7. Interrupt: Please Stop Soon
Java 17 deprecates Thread.stop() because forced termination can violate object invariants. This lesson uses interruption for cooperative cancellation.
worker.interrupt();This is just a request. The task must check at the appropriate point:
while (!Thread.currentThread().isInterrupted()) {
processNextBook();
}If the thread is blocking on sleep(), wait(), join(), or many concurrent tools' interruptible methods, they will throw InterruptedException. When this exception is thrown, the interrupt status is cleared.
Experiment: Restore Interrupt Status
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
public class InterruptionDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch ready = new CountDownLatch(1);
AtomicBoolean restored = new AtomicBoolean();
CountDownLatch neverReleased = new CountDownLatch(1);
Thread worker = new Thread(() -> {
try {
ready.countDown();
neverReleased.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
restored.set(Thread.currentThread().isInterrupted());
}
}, "interruptible-worker");
worker.start();
ready.await();
worker.interrupt();
worker.join();
System.out.println("Interrupt has been restored: " + restored.get());
if (!restored.get()) {
throw new AssertionError("Interrupt status not restored");
}
System.out.println("Interrupt check passed");
}
}Output:
Interrupt has been restored: true
Interrupt check passedready signals that the task is ready. neverReleased is never counted down, so interruption is the only way out of its wait. An interrupt delivered before await() also makes that call throw; correctness does not depend on guessing scheduling delays. The catch block cannot throw a checked exception upward, so it restores the interrupt status, allowing the more outer task framework to still observe the cancellation.
If your method signature allows it, propagating it more directly is clearer:
void awaitReceipt(Thread worker) throws InterruptedException {
worker.join();
}8. Where to Start with Thread Safety
Threads themselves do not automatically create problems. The danger comes from mutable shared state:
- Local variables belong to the current call stack, but their referenced objects may still be shared;
- Objects that are created and remain unchanged are easier to share safely;
- Multiple threads reading the same immutable data usually do not require locking;
- When multiple threads read, write, or modify the same inventory, balance, or collection, synchronization rules must be defined.
Thread initiation also has a publish guarantee: the calling thread’s actions before start() happen-before actions in the new thread. Therefore, it is easier to reason about tasks when fully constructing the data needed for the task before starting the thread, rather than modifying fields without synchronization after the thread is started.
9. Common Failures
| Phenomenon | Cause and Resolution |
|---|---|
After calling run(), the thread name remains main | run() is a regular call; use start() |
The second start() fails | Thread is one-time; create a new object or use an executor |
After using sleep(), tests still fail sporadically | Time delay is not a completion relationship; switch to a clear collaboration tool |
| After catching an interruption, the service won't shut down | The interruption was swallowed; propagate or restore the state |
| The JVM doesn't exit after the main method ends | There are still live user threads or the thread pool hasn't been closed |
| Files written by a daemon thread are only written halfway | Daemon threads are not guaranteed to clean up; explicitly stop and wait for them |
10. Exercise
- Create two threads named
catalog-workerandreceipt-worker. Do not assume any order of their output; use twojoin()to wait for all to complete. - Run the "double
start()" failure example, record the exception type. Explain why callingrun()twice does not trigger the same exception. - Add an interrupt check to a loop that processes 100 books at a time. Compare the applicability of "propagating
InterruptedException" with "capturing and restoring state". - Replace
sleep()withCountDownLatch, release both workers only after they are ready; the scheduler still decides when each actually runs.
Verify Startup, Completion, and Cancellation
First, use the thread name to prove that a direct call to run() remains in main, while start() enters a new execution path. When waiting for two worker threads, call join() separately, and only assert that they are both complete, without asserting the order of their outputs.
Next, have the same Thread start twice, confirming that the second call throws IllegalThreadStateException. Then, replace the fixed sleep with CountDownLatch, allowing the main thread to determine that the worker thread is ready before sending an interrupt. When capturing InterruptedException, either continue propagating it or restore the interrupt and complete the current task as soon as possible.
Thread states are only for observation. If a business code segment needs to repeatedly read getState() to know what to do next, it typically indicates a lack of true completion, message, or condition protocol.
Reviewing the Formal Contract of Java 17
Thread: Start, wait, interrupt, daemon properties and uncaught exceptions.Thread.State: Six JVM thread states.- JLS 17.4.5: happens-before order:
start,join, visibility relationships with locks and volatile. CountDownLatch: Using events to replace time guessing.
Next Step: Protecting Common Rules
A thread has a single lifecycle: start() starts another execution path, join() confirms completion, and interruption requests cancellation. The next lesson protects shared inventory when several threads try to borrow the last available book.
Continue learning about synchronization, communication, and deadlocks