Creating Threads in Java

13 Apr 2026, Updated: 05 Jul 2026 4 min read
2
There are multiple ways to create threads, each with its own use cases, advantages, and trade-offs.

In this article, we will explore three primary ways to create threads: extending the Thread class, implementing Runnable, and using Callable with Future. We will also discuss when to use each approach in real-world applications.

Extending Thread Class

The most straightforward way to create a thread in Java is by extending the Thread class and overriding its run() method. The logic inside run() defines the task that the thread will execute.
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running using Thread class");
    }
}

public class Main {
    static void main(String[] args) {
        MyThread t = new MyThread();
        t.start(); // starts a new thread
    }
}
Key Points:
- The start() method creates a new thread and internally calls run()
- Calling run() directly does NOT create a new thread
- This approach tightly couples task logic with thread implementation
Limitations:
- Java does not support multiple inheritance, so extending Thread prevents extending other classes
- Less flexible and not commonly used in modern applications

Implementing Runnable Interface

A more flexible and widely used approach is implementing the Runnable interface. This separates the task from the thread, promoting better design.
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread running using Runnable");
    }
}

public class Main {
    static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
} 
You can also use lambda expressions for cleaner code:
public class Main {
    static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Thread using lambda Runnable"));
        t.start();
    }
}
Advantages:
- Allows extending another class
- Promotes separation of concerns
- Better suited for thread pools and executor frameworks

Using Callable & Future

Both Thread and Runnable do not return results and cannot throw checked exceptions. To overcome this limitation, Java provides the Callable interface along with Future.

Callable represents a task that returns a result, while Future represents the result of an asynchronous computation.
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable {
    @Override
    public Integer call() {
        return 42;
    }
}

public class Main {
    static void main(String[] args) throws Exception {
        MyCallable task = new MyCallable();
        FutureTask futureTask = new FutureTask<>(task);
        Thread t = new Thread(futureTask);
        t.start();
        Integer result = futureTask.get(); // waits for result System.out.println("Result: " + result);
    }
}
Key Features:
- Can return a value
- Can throw checked exceptions
- Works well with ExecutorService (preferred in real-world scenarios)

When to Use What (Best Practices)

Choosing the right approach depends on the use case and application design.
Use Thread Class:
- Only for simple or quick demonstrations
- Rarely used in production code
- Avoid when flexibility is required
Use Runnable:
- When task does not return a result
- When you want to separate task logic from thread
- Ideal for use with thread pools and executors
Use Callable & Future:
- When task needs to return a result
- When exception handling is required
- For asynchronous and scalable applications

Modern Recommendation

In modern Java development, directly creating threads is often discouraged. Instead, developers use the Executor Framework to manage threads efficiently.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.submit(() -> {
            System.out.println("Task executed by thread pool");
        });
        executor.shutdown();
    }
} 
This approach provides better scalability, thread reuse, and resource management.

As we move forward, we will explore thread lifecycle in more detail and dive deeper into synchronization and thread communication mechanisms.
Nagesh Chauhan

Nagesh Chauhan

Principal Software Engineer • Java • Python • Distributed Systems • AI/ML

Principal Software Engineer with 14+ years of experience designing and delivering large-scale distributed systems, cloud-native applications, and AI-powered platforms.

Passionate about solving complex engineering problems using strong data structures and algorithms, along with expertise in Java, Spring Boot, Python, System Design, Microservices, Cloud, Kafka, Elasticsearch, and Generative AI.

Share this Article

💬 Comments

Join the Discussion