Skip to content

VOL 01 / CH 14 / LESSON 01

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.State values 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 ​

java
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():

java
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:

java
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:

bash
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:

text
Task thread: main
Task thread: library-worker
Thread check passed

The 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:

java
Thread worker = new Thread(() -> { });
worker.start();
worker.join();
worker.start(); // IllegalThreadStateException

Exceptions 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 ​

text
NEW --start()--> RUNNABLE -----------------------> TERMINATED
                    |  ^
                    |  | Acquire monitor/condition satisfied/time-out
                    v  |
             BLOCKED / WAITING / TIMED_WAITING
StateTypical ReasonKey Difference
NEWAfter new Thread(...) and before start()Not yet started
RUNNABLERunnable or executingJava has no separate RUNNING state; some OS-resource waits also appear here
BLOCKEDWaiting to enter synchronized regionWaiting for a monitor lock
WAITINGwait(), join(), park() without timeoutWaiting for another action, no deadline
TIMED_WAITINGsleep(), timed wait()/join()Has a deadline
TERMINATEDrun() returns or exits due to an uncaught exceptionCannot be restarted again

Thread State Snapshots Cannot Be Used for Coordination ​

java
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 ​

java
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:

java
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.

java
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.

java
worker.interrupt();

This is just a request. The task must check at the appropriate point:

java
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 ​

java
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:

text
Interrupt has been restored: true
Interrupt check passed

ready 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:

java
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 ​

PhenomenonCause and Resolution
After calling run(), the thread name remains mainrun() is a regular call; use start()
The second start() failsThread is one-time; create a new object or use an executor
After using sleep(), tests still fail sporadicallyTime delay is not a completion relationship; switch to a clear collaboration tool
After catching an interruption, the service won't shut downThe interruption was swallowed; propagate or restore the state
The JVM doesn't exit after the main method endsThere are still live user threads or the thread pool hasn't been closed
Files written by a daemon thread are only written halfwayDaemon threads are not guaranteed to clean up; explicitly stop and wait for them

10. Exercise ​

  1. Create two threads named catalog-worker and receipt-worker. Do not assume any order of their output; use two join() to wait for all to complete.
  2. Run the "double start()" failure example, record the exception type. Explain why calling run() twice does not trigger the same exception.
  3. 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".
  4. Replace sleep() with CountDownLatch, 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 ​

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

Built with VitePress | Software Systems Atlas