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 theThread 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:
- Thestart() 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 extendingThread 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 theRunnable 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 frameworksUsing Callable & Future
BothThread 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. Understanding these best practices is critical for writing efficient and maintainable concurrent programs.Use Thread Class:
- Only for simple or quick demonstrations - Rarely used in production code - Avoid when flexibility is requiredUse Runnable:
- When task does not return a result - When you want to separate task logic from thread - Ideal for use with thread pools and executorsUse Callable & Future:
- When task needs to return a result - When exception handling is required - For asynchronous and scalable applicationsModern 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.
Java provides multiple ways to create threads, each designed for different scenarios. While extending
Thread is simple, implementing Runnable offers better flexibility, and using Callable with Future enables powerful asynchronous programming.
Understanding when to use each approach is essential for writing efficient, scalable, and maintainable multithreaded applications. As we move forward, we will explore thread lifecycle in more detail and dive deeper into synchronization and thread communication mechanisms.
Join the discussion