Java Threads and Lifecycle Explained with Examples

A Java thread is an independent path of execution within a process.

Lesson 88 of 127Advanced13 min read

A Java thread is an independent path of execution within a process. Starting another thread lets work proceed concurrently with the thread running main, but it also introduces scheduling and coordination questions. Calling a task's run method directly is an ordinary call; calling Thread.start requests a new thread that executes run. This lesson uses short, joined tasks so the examples finish predictably and do not rely on whichever thread happens to run first.

Create a task separately from its thread

Runnable describes an action with no return value. A Thread can receive a Runnable and execute it after start. Keeping the task separate makes it easier to test directly or submit later to an executor. A Thread object can be started only once. Calling start again throws IllegalThreadStateException even after the first run has completed. To repeat the action, create another thread or use an executor that manages reusable worker threads.

Recognize the lifecycle states

Thread.State includes NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. NEW means start has not been called. RUNNABLE includes a thread running or ready to run. BLOCKED specifically means waiting to acquire a monitor lock, while WAITING and TIMED_WAITING describe other waiting conditions. State inspection is a momentary observation, not a reliable synchronization mechanism. A state can change immediately after getState returns.

Wait for completion with join

join makes the calling thread wait for the target thread to finish. In the first example, main joins the worker before printing Finished, so the worker's message is guaranteed to appear first. A successful join also establishes visibility of work completed by the terminated thread. Sleeping for a guessed duration is not an equivalent replacement: a slow machine may leave the worker unfinished even after that delay.

Understand sleep and interruption

Thread.sleep pauses the current thread for approximately the requested duration and does not release a monitor lock it holds. It can throw InterruptedException if interrupted. Interruption is a cooperative cancellation signal, not a forced termination command. A task should decide how to respond, usually by stopping cleanly and preserving its interrupt status when it cannot propagate the exception. Never depend on a precise sleeping interval for correctness or output order.

Protect shared state explicitly

Two threads can access the same object, so ordinary field updates may race. Incrementing an int is a read, calculation, and write, not one guaranteed atomic operation. join ensures completion and visibility afterward, but it does not prevent lost updates while workers run. Use synchronization or suitable atomic classes for shared modifications. A simpler design often gives each task its own data and combines results after completion.

Choose managed execution for larger applications

Creating one thread for every incoming task can exhaust resources. ExecutorService provides task submission and lifecycle management and is a better next step for many applications. Java 21 introduced virtual threads, which have different scaling characteristics for blocking tasks; these Java 17 examples use platform threads. Regardless of thread type, concurrency does not remove the need to protect shared data or define cancellation and completion boundaries.

Practice tasks and checks

Replace the empty lifecycle task with a loop that computes a subtotal locally. Store its result in an object, join the worker, and only then read the result from main. Explain why this boundary is appropriate and why inspecting getState in a busy loop is not. Next, deliberately call run directly and print Thread.currentThread().getName inside the task; the name should identify the caller instead of a new worker. Restore start afterward. Finally, interrupt a sleeping worker and verify that the program terminates promptly rather than waiting for the full requested sleep. Keep the join so your test observes actual termination. Never write an assessment that expects a particular state immediately after start, since that state depends on scheduling.

Java Summary

  • start requests a new thread; run directly executes on the caller.
  • join establishes completion before a caller uses the result.
  • Interruption is cooperative and does not force arbitrary code to stop.
  • Continue with executor-managed tasks when these examples are clear.

Java Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Start and join one worker

Save this program as JoinedWorker.java. Compile it with javac JoinedWorker.java and run it with java JoinedWorker.

Expected output:

Constructing worker creates a NEW thread but does not run the task. start schedules its execution. main then waits at join until the worker's print action and thread termination are complete. Only afterward does main print Finished. The worker may begin before main reaches join, which does not change the guaranteed final order. This is a completion relationship, not a timing guess based on a sleep.

Java
public class JoinedWorker {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(
            () -> System.out.println("Packing complete"), "packer");
        worker.start();
        worker.join();
        System.out.println("Finished");
    }
}
Output
Packing complete
Finished
Example 2

Observe stable lifecycle boundaries

Save this program as LifecycleBoundary.java. Compile it with javac LifecycleBoundary.java and run it with java LifecycleBoundary.

Expected output:

The first state is inspected before start, so no scheduler can have advanced this thread yet. The second is inspected after join has confirmed termination. These are stable boundaries. Inspecting immediately after start could instead show RUNNABLE or TERMINATED depending on timing, which is why the example avoids promising an intermediate state. Empty work can complete before the caller makes its next observation.

Java
public class LifecycleBoundary {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {});
        System.out.println(worker.getState());
        worker.start();
        worker.join();
        System.out.println(worker.getState());
    }
}
Output
NEW
TERMINATED
Example 3

Request cooperative cancellation

Save this program as InterruptedWorker.java. Compile it with javac InterruptedWorker.java and run it with java InterruptedWorker.

Expected output:

The interrupt signal is set whether the worker has reached sleep or is about to enter it. sleep responds with InterruptedException. The catch prints a cancellation message and restores the flag, then the task ends. join ensures Worker stopped is printed last. Restoring the flag preserves the signal for surrounding code; in this tiny task there is no further work, but the same pattern is useful when a helper cannot propagate interruption.

Java
public class InterruptedWorker {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            try {
                Thread.sleep(10_000);
            } catch (InterruptedException error) {
                System.out.println("Cancellation received");
                Thread.currentThread().interrupt();
            }
        });
        worker.start();
        worker.interrupt();
        worker.join();
        System.out.println("Worker stopped");
    }
}
Output
Cancellation received
Worker stopped

Java FAQ

Does calling run start a new thread?

No. It runs the task on the calling thread like any other method. start requests a separate thread and arranges for its run method to execute there.

Why not use sleep to wait for a task?

A delay only postpones the caller. It does not establish that the task completed or provide the completion visibility guarantee that join supplies. Use a completion API instead of guessing a sufficient delay.

What are virtual threads?

Virtual threads, standard since Java 21, are lightweight threads managed by the JVM rather than the operating system. Start one with Thread.ofVirtual().start(task), or run each task in its own with Executors.newVirtualThreadPerTaskExecutor(). They make it practical to run thousands of tasks that spend most of their time waiting for I/O.

What is a daemon thread?

A background thread that doesn't keep the program running. The JVM exits when only daemon threads remain, even if they haven't finished. Mark a thread with setDaemon(true) before starting it, and only use this for work that can safely be abandoned, such as a periodic cache clean-up.

Can a Java thread be started twice?

No. Calling start() on a thread that has already been started throws IllegalThreadStateException, even after it has finished. Create a new Thread for each run, or better, submit the Runnable to an ExecutorService as often as you need.

Java Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

Which operation requests execution on a new thread?

Multiple choicemedium

Why is Finished printed after Packing complete?

Java Quiz

2 questions to check what you learned.

  1. Can the same Thread object be started again after termination?

    • No
    • Yes, after join
    • Yes, if it has a new name
  2. Does join alone prevent two workers losing increments?

    • No; updates still need coordination
    • Yes; it makes every write atomic
    • Only for int fields