How to Create a Thread in Java – Thread, Runnable, Callable and Virtual Threads
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():
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");
}
}
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:
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();
}
}
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:
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());
}
}
}
}
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 inExecutionException.ExecutorServiceimplementsAutoCloseablesince 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:
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));
}
}
platform-1 virtual? false virtual-1 virtual? true startVirtualThread ran 10,000 sleeping tasks finished in under 1 s: true
Thread.ofPlatform()andThread.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():
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();
}
}
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?
| Approach | Returns a value | Reusable task | Best for |
|---|---|---|---|
Extend Thread | No | No | Quick demos; rarely in production |
Implement Runnable / lambda | No | Yes | Simple fire-and-forget background work |
Callable + ExecutorService | Yes | Yes | Most application code: pooled, managed tasks |
CompletableFuture | Yes | Yes | Chaining and combining asynchronous steps |
| Virtual threads | Yes (via executor) | Yes | Large 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
Threador, preferably, by passing aRunnable(often a lambda). - Use
Callablewith anExecutorServiceto get results and exceptions back. - Java 21 virtual threads make one-thread-per-task practical for I/O-bound work.
- Always call
start(), notrun(), to create a new thread.
Related Tutorials
Frequently Asked Questions (FAQ)
Q1. How many ways are there to create a thread in Java?
Fundamentally two: extend Thread or supply a Runnable. In practice you also use lambdas, Callable with executors, CompletableFuture and the Java 21 Thread.Builder for virtual threads.
Q2. Which is better: extending Thread or implementing Runnable?
Implementing Runnable. It keeps your class free to extend another class, separates the task from the thread, and works with thread pools.
Q3. What is the difference between Runnable and Callable?
Runnable.run() returns nothing and cannot throw checked exceptions. Callable.call() returns a value and can throw checked exceptions.
Q4. What happens if I call run() instead of start()?
The code runs in the current thread like a normal method call; no new thread is created.
Q5. Can I start a thread twice?
No. A second start() call throws IllegalThreadStateException. Create a new Thread instead.
Q6. What is a Future?
An object representing the pending result of an asynchronous task. get() waits for the result; cancel() attempts to stop the task.
Q7. How do I create a thread with a lambda?
new Thread(() -> doWork()).start();, because Runnable is a functional interface.
Q8. What is a virtual thread?
A lightweight thread introduced in Java 21, scheduled by the JVM onto a small number of OS threads. Blocking it is cheap, so millions can exist.
Q9. How do I create a virtual thread?
Use Thread.ofVirtual().start(task), Thread.startVirtualThread(task), or Executors.newVirtualThreadPerTaskExecutor().
Q10. Should virtual threads be pooled?
No. They are cheap to create, so create one per task. Use semaphores if you need to limit concurrent access to a resource.
Q11. Can run() throw a checked exception?
No. Runnable.run() declares no checked exceptions, so they must be caught inside. Use Callable to propagate them.
Q12. How do I get a result from a thread?
Submit a Callable to an ExecutorService and call get() on the returned Future, or use CompletableFuture.supplyAsync().
Q13. What happens to exceptions thrown in a thread?
An uncaught exception ends that thread and is passed to its UncaughtExceptionHandler, which by default prints the stack trace. Other threads continue.
Q14. Why name threads?
Names appear in logs, thread dumps and profilers, making it much easier to understand what each thread is doing.
Q15. What is the Thread.Builder API?
A Java 21 API, reached through Thread.ofPlatform() and Thread.ofVirtual(), for configuring and creating threads or thread factories fluently.
Q16. Do I need to shut down an ExecutorService?
Yes. Call shutdown() (or use try-with-resources on Java 19+); otherwise its non-daemon threads can keep the JVM running.
Q17. Is the main method a thread?
It runs on the main thread, which the JVM creates when the program starts.
Q18. Can an anonymous class create a thread?
Yes: new Thread() { public void run() { ... } }.start(); or an anonymous Runnable. Lambdas are shorter.
Q19. Are virtual threads faster than platform threads?
Not for a single computation. They improve throughput when many tasks spend their time waiting on I/O.
Q20. What is CompletableFuture?
A Future that can be completed and composed: supplyAsync, thenApply, thenCombine and exceptionally build non-blocking pipelines.