Project Based Learning in Java

Java Threads and Their Lifecycle

Concurrency, thread creation, start versus run, and the six JVM thread states.

Lecture 2.1.3 CO3 aligned Unit 2 · Chapter 2.1

Learning outcome: Explain concurrency, create threads with Thread and Runnable, distinguish start() from run(), and identify the six official JVM thread states.

A thread is one path of execution inside a process. A Java application begins with a thread that runs main, and it can start additional threads so independent tasks can make progress concurrently.

In simple terms: a process is the whole shop; a thread is one worker inside it. Every Java program opens with a single worker, the main thread. Starting more threads is hiring more workers, so a file download and a screen update can proceed without either waiting for the other.

Process, Thread, Concurrency, and Parallelism

Term Meaning
Process A running program with its own resources and memory space
Thread A path of execution inside a process
Concurrency Multiple tasks make progress during overlapping time
Parallelism Multiple tasks execute at the same instant on different processing cores

Concurrency or parallelism? One cashier serving three queues by switching between them is concurrency: everyone makes progress, but only one person is served at any instant. Three cashiers at three counters is parallelism. Concurrency is a way of structuring a program; parallelism additionally needs several processor cores.

Threads inside one process share heap objects and other process resources, while each thread has its own call stack. Sharing makes communication efficient but also creates the possibility of race conditions when several threads update the same mutable data.

In simple terms: shared heap, private stack. Objects on the heap are the shop's common counter that every worker can touch, while each worker's local variables and method calls are a private notepad. Almost every threading problem starts at the common counter.

Creating a Thread by Extending Thread

One method is to subclass Thread, override run, create an object, and call start.

public class MessageThread extends Thread {
    @Override
    public void run() {
        System.out.println("Worker: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) throws InterruptedException {
        MessageThread worker = new MessageThread();
        worker.setName("message-worker");
        worker.start();
        worker.join();
        System.out.println("Main finished");
    }
}

Output:

Worker: message-worker
Main finished

start() schedules a new thread and the JVM invokes its run() method. The join() call makes this example wait for the worker before printing the final line.

Extending Thread is simple for demonstrations, but the class cannot then extend another class.

In simple terms: extending Thread makes the task be a worker. Implementing Runnable makes the task a job card that any worker can pick up. Java allows only one superclass, so the first choice spends that single slot on plumbing rather than on the problem being solved.

Creating a Thread with Runnable

Runnable separates the task from the thread that executes it.

public class RunnableDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            String name = Thread.currentThread().getName();
            System.out.println("Running in " + name);
        };

        Thread worker = new Thread(task, "report-worker");
        worker.start();
        worker.join();
    }
}

Output:

Running in report-worker

The lambda expression is a compact implementation of the single run() method in Runnable.

Thread vs Runnable

Approach Strength Limitation
Extend Thread Direct and easy to demonstrate Task and execution mechanism are coupled; no other superclass can be extended
Implement Runnable Separates work from the thread; works with lambdas and executors Requires a Thread or executor to run the task

Prefer Runnable for most application tasks. Extend Thread only when the thread object itself needs specialised behaviour.

start() vs run()

Calling start() and calling run() are not equivalent.

Call What happens
worker.start() A new thread is scheduled and later executes run()
worker.run() An ordinary method call occurs on the current thread
public class StartVsRun {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () ->
                System.out.println(Thread.currentThread().getName());

        Thread first = new Thread(task, "new-worker");
        first.start();
        first.join();

        Thread second = new Thread(task, "never-started");
        second.run();
    }
}

Output:

new-worker
main

In simple terms: start() hands the job card to a new worker and returns at once. run() is an ordinary method call: you do the job yourself, on the spot, and no new thread is created. That is exactly why the second line of output reads main.

The direct run() call executes on the main thread. A thread can be started only once. Calling start() again throws IllegalThreadStateException.

The Six JVM Thread States

Java defines six states in Thread.State.

State Meaning
NEW Created but not started
RUNNABLE Eligible to run or currently executing in the JVM
BLOCKED Waiting to acquire a monitor lock
WAITING Waiting indefinitely for another thread to act, such as untimed join() or wait()
TIMED_WAITING Waiting for a bounded time, such as during sleep()
TERMINATED Execution has completed

There is no separate RUNNING value in the official enum. A thread using the CPU and a runnable thread waiting for processor time both appear as RUNNABLE.

Telling the waiting states apart: BLOCKED means waiting for a lock that another thread holds. WAITING means waiting for another thread to finish or to signal, with no time limit. TIMED_WAITING is the same kind of wait but with an alarm set, as during sleep(200).

Observing State Changes

public class StateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            try {
                Thread.sleep(200);
            } catch (InterruptedException exception) {
                Thread.currentThread().interrupt();
            }
        }, "state-worker");

        System.out.println(worker.getState());
        worker.start();
        Thread.sleep(20);
        System.out.println(worker.getState());
        worker.join();
        System.out.println(worker.getState());
    }
}

Typical output:

NEW
TIMED_WAITING
TERMINATED

Timing observations can vary. getState() is intended for monitoring and diagnosis, not for coordinating program correctness.

Why not coordinate with getState()? The value can already be out of date by the time it is returned, because the thread may change state in the same instant. When correctness depends on the answer, use join() or a lock instead.

Scheduling and Output Order

The thread scheduler decides when runnable threads execute. Unless code adds coordination, output order is not guaranteed.

public class TwoWorkers {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 1; i <= 3; i++) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        };

        Thread first = new Thread(task, "A");
        Thread second = new Thread(task, "B");
        first.start();
        second.start();
        first.join();
        second.join();
    }
}

The lines from A and B may interleave differently on different runs. This is normal concurrent behaviour.

Enrichment: Platform and Virtual Threads

The core examples use platform threads and compile on the JDK 17 course baseline. JDK 21 also provides virtual threads for large numbers of tasks that spend much of their time waiting for I/O. Virtual threads are enrichment; both kinds use the Thread abstraction, while the lifecycle and coordination principles remain the same.

Common Mistakes

Mistake Correction
Treating a thread as a separate process Threads share the process heap and resources.
Calling run() to start concurrency Call start() to schedule a new thread.
Starting the same Thread twice Create a new thread object for another execution.
Expecting a fixed order without coordination Scheduling order is not guaranteed.
Teaching only five informal states Use the six official Thread.State values.

Practice

  1. Create two named threads with Runnable lambdas and print each name.
  2. Observe the NEW and TERMINATED states of a worker.
  3. Call run() directly and explain why the current thread name is main.
  4. Run two counters several times and record how their output interleaves.

Quick Check

1. Which method schedules a new thread?

start() schedules a new thread, which then executes its run() method.

2. How many states are defined by Thread.State?

Six: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED.

3. Is there an official RUNNING state?

No. A thread executing in the JVM is represented by RUNNABLE.

4. Why is Runnable commonly preferred?

It separates the task from the execution mechanism and leaves the task class free to extend another class.

Lecture Quiz

A thread has been created but start() has not been called. What is its state?

Its state is NEW.

Summary

A thread is a path of execution inside a process. Java can create threads by extending Thread or, preferably, by supplying a Runnable. start() creates concurrent execution while direct run() does not. The official lifecycle contains six JVM states, and scheduling order is not guaranteed.