00:00

How to Create a Thread in Java – Thread, Runnable, Callable and Virtual Threads

Last updated: 7 min read

Java offers several ways to run code in a new thread. The two classic approaches are extending the Thread class and implementing the Runnable interface. Modern Java adds lambdas, Callable tasks run by an ExecutorService, and, since Java 21, virtual threads created with the Thread.Builder API.

This tutorial shows each approach with a runnable example, explains the difference between start() and run(), and helps you choose the right technique.

1. Extending the Thread Class

Create a subclass of Thread, override run() with the work, and call start():

ExtendThread.java
public class ExtendThread {
    // Way 1: extend Thread and override run()
    static class Downloader extends Thread {
        private final String file;
        Downloader(String file) { super("dl-" + file); this.file = file; }

        @Override
        public void run() {
            System.out.println(getName() + " downloading " + file);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Downloader d = new Downloader("report.pdf");
        d.start();          // creates a new thread that calls run()
        d.join();
        System.out.println("main: download thread finished");
    }
}
Output
dl-report.pdf downloading report.pdf
main: download thread finished

This is simple, but your class can no longer extend anything else, and it mixes the task (what to do) with the mechanism (the thread).

2. Implementing Runnable (and Lambdas)

Runnable is a functional interface with one method, void run(). Pass a Runnable to a Thread constructor, either as a class or as a lambda:

ImplementRunnable.java
public class ImplementRunnable {
    // Way 2: implement Runnable and pass it to a Thread
    static class EmailTask implements Runnable {
        private final String to;
        EmailTask(String to) { this.to = to; }
        @Override public void run() {
            System.out.println(Thread.currentThread().getName() + " sending email to " + to);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new EmailTask("asha@example.com"), "mailer");
        t1.start();
        t1.join();

        // Way 3: Runnable as a lambda
        Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + " says hello from a lambda"), "lambda-thread");
        t2.start();
        t2.join();
    }
}
Output
mailer sending email to asha@example.com
lambda-thread says hello from a lambda

This is the preferred classic approach: the task is separate from the thread, the class remains free to extend another class, and the same Runnable can be run by a thread pool.

3. Callable, Future and ExecutorService

Runnable cannot return a result or throw checked exceptions. Callable<V> can. Submit it to an ExecutorService (a thread pool) and receive a Future for the result:

CallableFuture.java
import java.util.concurrent.*;

public class CallableFuture {
    public static void main(String[] args) throws Exception {
        // Way 4: Callable returns a value (and may throw) - run it with an ExecutorService
        Callable<Integer> sumTask = () -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            return sum;
        };

        try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
            Future<Integer> future = pool.submit(sumTask);
            System.out.println("Doing other work while the task runs...");
            System.out.println("Sum 1..100 = " + future.get());   // waits for the result

            Future<?> failing = pool.submit(() -> { throw new IllegalStateException("service down"); });
            try {
                failing.get();
            } catch (ExecutionException e) {
                System.out.println("Task failed: " + e.getCause());
            }
        }
    }
}
Output
Doing other work while the task runs...
Sum 1..100 = 5050
Task failed: java.lang.IllegalStateException: service down
  • future.get() blocks until the result is ready; an exception thrown by the task arrives wrapped in ExecutionException.
  • ExecutorService implements AutoCloseable since Java 19, so try-with-resources shuts it down and waits for tasks.

4. Thread.Builder and Virtual Threads (Java 21)

Java 21 added a builder API and virtual threads: lightweight threads managed by the JVM rather than the operating system. Blocking a virtual thread is cheap, so you can run one per task, even for many thousands of tasks:

BuilderAndVirtual.java
import java.util.concurrent.*;

public class BuilderAndVirtual {
    public static void main(String[] args) throws Exception {
        // Way 5: Thread.Builder API (Java 21)
        Thread platform = Thread.ofPlatform().name("platform-1").start(
                () -> System.out.println(Thread.currentThread().getName() + " virtual? " + Thread.currentThread().isVirtual()));
        platform.join();

        Thread virtual = Thread.ofVirtual().name("virtual-1").start(
                () -> System.out.println(Thread.currentThread().getName() + " virtual? " + Thread.currentThread().isVirtual()));
        virtual.join();

        Thread quick = Thread.startVirtualThread(() -> System.out.println("startVirtualThread ran"));
        quick.join();

        // 10,000 virtual threads, each blocking for 100 ms: finishes in a fraction of a second
        long start = System.nanoTime();
        try (ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                ex.submit(() -> { Thread.sleep(100); return null; });
            }
        }
        long ms = (System.nanoTime() - start) / 1_000_000;
        System.out.println("10,000 sleeping tasks finished in under 1 s: " + (ms < 1000));
    }
}
Output
platform-1 virtual? false
virtual-1 virtual? true
startVirtualThread ran
10,000 sleeping tasks finished in under 1 s: true
  • Thread.ofPlatform() and Thread.ofVirtual() configure names, daemon status and more before starting.
  • Executors.newVirtualThreadPerTaskExecutor() creates a new virtual thread for each task, with no need to size a pool.
  • Virtual threads are ideal for I/O-bound work such as web requests and database calls. They do not speed up CPU-bound computation.

5. start() vs run()

A common mistake is calling run() instead of start():

RunVsStart.java
public class RunVsStart {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> System.out.println("run() executes in: " + Thread.currentThread().getName()), "worker");
        t.run();     // just a normal method call - runs in main
        t.start();   // new thread - runs in worker
        t.join();
    }
}
Output
run() executes in: main
run() executes in: worker

run() is an ordinary method call on the current thread. Only start() creates a new thread of execution. Calling start() twice on the same object throws IllegalThreadStateException.

6. Which Approach Should You Use?

ApproachReturns a valueReusable taskBest for
Extend ThreadNoNoQuick demos; rarely in production
Implement Runnable / lambdaNoYesSimple fire-and-forget background work
Callable + ExecutorServiceYesYesMost application code: pooled, managed tasks
CompletableFutureYesYesChaining and combining asynchronous steps
Virtual threadsYes (via executor)YesLarge numbers of blocking, I/O-bound tasks

7. Useful Thread Methods

  • setName / getName: give threads meaningful names for logs.
  • join(): wait for a thread to finish.
  • Thread.sleep(ms): pause the current thread.
  • interrupt() / isInterrupted(): request and detect cancellation.
  • setDaemon(true): make a background thread that does not keep the JVM alive (see Daemon Thread).
  • setPriority(n): a scheduling hint (see Thread Priority).

8. Summary

  • Create threads by extending Thread or, preferably, by passing a Runnable (often a lambda).
  • Use Callable with an ExecutorService to get results and exceptions back.
  • Java 21 virtual threads make one-thread-per-task practical for I/O-bound work.
  • Always call start(), not run(), to create a new thread.

Related Tutorials