Skip to content
Java multithreading 7 min read

Creating a Thread

Java gives you several ways to create and run a new thread. Choosing the right approach depends on whether your task needs to return a result, throw checked exceptions, or extend another class. This page walks you through all three techniques with working examples — and explains what the JVM is doing behind the scenes when you call start().

The Two Core Approaches (and One Modern One)

ApproachHowReturns a value?Can throw checked exceptions?
Extend ThreadOverride run() in a subclassNoNo
Implement RunnablePass a Runnable to new Thread(...)NoNo
Use Callable + FutureSubmit to an ExecutorServiceYesYes

Tip: Prefer Runnable over extending Thread in most cases — it keeps your class free to extend something else and separates the task from the thread mechanism. Use Callable when you need a return value or must propagate exceptions.


Approach 1 — Extending the Thread Class

The simplest way: create a subclass of Thread and override its run() method. Then instantiate your class and call start().

class CounterThread extends Thread {
    private final String label;

    CounterThread(String label) {
        this.label = label;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(label + " → " + i);
        }
    }
}

public class ExtendThreadDemo {
    public static void main(String[] args) {
        CounterThread t1 = new CounterThread("Thread-A");
        CounterThread t2 = new CounterThread("Thread-B");
        t1.start();
        t2.start();
    }
}

Output (order may vary between runs):

Thread-A → 1
Thread-B → 1
Thread-A → 2
Thread-B → 2
Thread-A → 3
Thread-B → 3

Warning: Never call run() directly — that executes the method on the current thread, not a new one. Always call start() to actually spawn a new thread. See run() vs start() for a full explanation.

When to use this approach

  • Quick experiments or small programs where you won’t need to extend another class
  • When you want convenient access to Thread methods (getName(), setPriority(), etc.) without casting

The limitation

Java supports only single inheritance. If your class already extends something (e.g., JFrame, a base entity), you cannot also extend Thread. In that case, Runnable is the right choice.


Approach 2 — Implementing Runnable

Runnable is a functional interface with a single method: void run(). You implement it and pass an instance to a Thread constructor.

class GreetTask implements Runnable {
    private final String name;

    GreetTask(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("Hello from " + Thread.currentThread().getName()
                           + " — greeting: " + name);
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(new GreetTask("Alice"), "WorkerThread-1");
        Thread t2 = new Thread(new GreetTask("Bob"),   "WorkerThread-2");
        t1.start();
        t2.start();
    }
}

Output:

Hello from WorkerThread-1 — greeting: Alice
Hello from WorkerThread-2 — greeting: Bob

Using a Lambda (Java 8+)

Because Runnable is a functional interface, you can write it as a lambda for concise one-off tasks:

public class LambdaThreadDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> System.out.println("Task 1 running on: "
                                + Thread.currentThread().getName()));
        Thread t2 = new Thread(() -> System.out.println("Task 2 running on: "
                                + Thread.currentThread().getName()));
        t1.start();
        t2.start();
    }
}

Output:

Task 1 running on: Thread-0
Task 2 running on: Thread-1

Tip: Give your threads meaningful names (second argument to the Thread constructor) — it makes logs and stack traces much easier to read. See Naming Threads.


Approach 3 — Using Callable and Future

Runnable.run() cannot return a value or throw a checked exception. When you need either of those things, use Callable<V> together with an ExecutorService.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class SumTask implements Callable<Integer> {
    private final int limit;

    SumTask(int limit) {
        this.limit = limit;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= limit; i++) sum += i;
        System.out.println("Computed on: " + Thread.currentThread().getName());
        return sum;
    }
}

public class CallableDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Future<Integer> future1 = executor.submit(new SumTask(100));
        Future<Integer> future2 = executor.submit(new SumTask(200));

        // future.get() blocks until the result is ready
        System.out.println("Sum 1-100 = " + future1.get());
        System.out.println("Sum 1-200 = " + future2.get());

        executor.shutdown();
    }
}

Output:

Computed on: pool-1-thread-1
Computed on: pool-1-thread-2
Sum 1-100 = 5050
Sum 1-200 = 20100

Future.get() blocks the calling thread until the result is available. If the Callable threw an exception, get() re-throws it wrapped in an ExecutionException.

Note: Callable and Future are part of java.util.concurrent, introduced in Java 5. They are the foundation of modern async Java — see Callable & Future for a deep dive.


Quick Comparison: All Three Side by Side

// 1. Extend Thread
new Thread() {
    public void run() { System.out.println("Via Thread subclass"); }
}.start();

// 2. Implement Runnable (lambda shorthand)
new Thread(() -> System.out.println("Via Runnable lambda")).start();

// 3. Callable via ExecutorService
ExecutorService exec = Executors.newSingleThreadExecutor();
Future<String> f = exec.submit(() -> "Via Callable");
System.out.println(f.get());   // prints: Via Callable
exec.shutdown();

Common Mistakes

Calling run() instead of start()

Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run();   // prints "main" — no new thread created!
t.start(); // prints "Thread-0" — correct

Starting the same Thread instance twice

Once a thread reaches the TERMINATED state you cannot restart it. Calling start() a second time throws IllegalThreadStateException. See Starting a Thread Twice.

Thread t = new Thread(() -> System.out.println("running"));
t.start();
t.join();
t.start(); // throws IllegalThreadStateException

Under the Hood

What start() Actually Does

When you call t.start(), the JVM:

  1. Checks the thread is in NEW state (throws IllegalThreadStateException otherwise)
  2. Allocates a native OS thread and a per-thread stack (typically 256 KB–1 MB depending on the platform and -Xss JVM flag)
  3. Registers the thread with the JVM’s thread registry
  4. Hands the thread to the OS scheduler, which eventually calls run() on the new native thread

This is why thread creation is relatively expensive — it involves a system call. If you need many short-lived tasks, use a Thread Pool to amortize the cost.

Thread Object vs OS Thread

A Thread object is a regular Java heap object — it holds metadata (name, priority, daemon flag, state). The actual execution runs on a native OS thread. The two are linked via a private native reference inside the Thread object.

Thread t = new Thread(task);   // Java heap object created — state = NEW
t.start();                     // native OS thread spawned — state = RUNNABLE
                               // OS scheduler runs run() — state = RUNNING (internal)
                               // run() returns             — state = TERMINATED
                               // native thread destroyed, Java object GC-eligible

The Runnable Inside Thread

Thread itself implements Runnable. When you extend Thread and override run(), the overridden method is called directly. When you pass a Runnable to the constructor, it is stored in a field called target. The default Thread.run() implementation is:

// Simplified from java.lang.Thread source
public void run() {
    if (target != null) {
        target.run();
    }
}

This is the Template Method pattern — extending Thread replaces the whole method; supplying a Runnable fills in the target slot.

Virtual Threads (Java 21)

Java 21 introduced virtual threads via Project Loom. You create them the same way, but they are managed by the JVM — not mapped 1:1 to OS threads. This makes it practical to run millions of concurrent tasks:

// Java 21+: create a virtual thread
Thread vt = Thread.ofVirtual().name("my-virtual-thread").start(() ->
    System.out.println("Running on: " + Thread.currentThread())
);
vt.join();

Virtual threads share the same Thread API, so migration from platform threads is mostly mechanical. See Virtual Threads for the full picture.


  • run() vs start() — Understand the critical difference and why calling run() directly does not create a new thread.
  • Thread Life Cycle — A full walkthrough of every state a thread passes through from NEW to TERMINATED.
  • Callable & Future — Return values from concurrent tasks and handle exceptions without losing them.
  • Thread Pools & Executors — The production-ready way to manage many threads without creating them manually.
  • Naming Threads — Give threads descriptive names to make debugging and log analysis far easier.
  • Virtual Threads (Project Loom) — Java 21’s lightweight threads that let you create millions of concurrent tasks cheaply.
Last updated June 13, 2026
Was this helpful?