Concurrency with Threads and Runnables in Java
Java provides powerful tools for achieving concurrency – the ability to execute multiple tasks seemingly simultaneously. Two fundamental ways to create concurrent tasks are by extending the Thread
class or implementing the Runnable
interface. This tutorial will explore both approaches, outlining their differences, benefits, and when to choose one over the other.
Understanding Threads and Runnables
At its core, a thread represents a single flow of execution within a program. The Java Virtual Machine (JVM) manages these threads, allowing them to run concurrently.
Thread
Class: Directly extending theThread
class creates a new type of thread. You override therun()
method to define the task this thread will execute.Runnable
Interface: TheRunnable
interface is a functional interface with a single method,run()
. ImplementingRunnable
allows you to define a task that can be executed by a thread. You then create aThread
object and pass yourRunnable
instance to it.
Implementing Threads vs. Runnables: The Code
Let’s examine how both approaches look in code.
Extending Thread
:
public class MyThread extends Thread {
@Override
public void run() {
// Code to be executed by the thread
System.out.println("MyThread is running");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // Starts the thread, calling the run() method in a new thread of execution
}
}
Implementing Runnable
:
public class MyRunnable implements Runnable {
@Override
public void run() {
// Code to be executed by the thread
System.out.println("MyRunnable is running");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // Starts a new thread, executing the run() method of MyRunnable
}
}
Both code snippets achieve the same outcome: a new thread is created and executes a specified task. However, there’s a crucial difference in terms of design and flexibility.
Key Differences and Considerations
1. Flexibility and Inheritance:
The most significant difference lies in flexibility. Java only supports single inheritance – a class can extend only one other class. If your class needs to extend another class for any reason, you cannot directly extend Thread
. However, you can implement Runnable
and still extend another class, providing greater design freedom.
2. Separation of Concerns:
Implementing Runnable
promotes a clearer separation of concerns. It separates the task (what the thread does) from the thread itself (how the task is executed). This makes your code more modular and easier to test. You can reuse the same Runnable
instance with different threads or even execute it in a single-threaded environment without modification.
3. Reusability:
Because Runnable
is an interface, you can easily pass instances of classes implementing it to thread pools or other concurrent execution frameworks. This reusability is a major advantage in complex applications.
4. Functional Programming:
With the introduction of lambda expressions in Java 8, implementing Runnable
becomes even more concise:
Runnable myRunnable = () -> System.out.println("Runnable with lambda");
new Thread(myRunnable).start();
Modern Concurrency: Beyond Raw Threads
While directly creating and managing threads is a fundamental concept, modern Java concurrency often utilizes higher-level abstractions like ExecutorService
. ExecutorService
manages a pool of threads, simplifying thread management and improving performance. ExecutorService
typically accepts Runnable
or Callable
tasks.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Runnable task = () -> System.out.println("Task executed by the executor");
executor.execute(task);
executor.shutdown(); // Important to shutdown the executor when finished
}
}
Best Practices
- Prefer
Runnable
over extendingThread
: Unless you have a specific reason to extendThread
, implementingRunnable
provides greater flexibility and promotes better design. - Use
ExecutorService
for thread management: LeverageExecutorService
to simplify thread management, improve performance, and avoid common concurrency pitfalls. - Consider
Callable
for tasks that return a value: TheCallable
interface is similar toRunnable
, but it allows the task to return a result. - Handle exceptions appropriately: Properly handle exceptions within your threads to prevent unexpected crashes and ensure application stability.