Using the Runnable
interface allows you to define a task that can be executed by different threads or even used with advanced concurrency mechanisms provided by the Java Concurrency API, such as ExecutorService
. This makes your task implementation more versatile and reusable across various execution strategies.
Example with Runnable
Let’s say we have a simple Runnable
task that prints out numbers from 1 to 5 with a delay between each number. We will then execute this task using different strategies: directly with a Thread
and with an ExecutorService
.
Defining the Runnable
Task
class NumberPrinter implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Number: " + i);
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Set the interrupt flag
System.out.println("Interrupted!");
break;
}
}
}
}
Execution Strategy 1: Using Thread
public class DirectThreadExample {
public static void main(String[] args) {
NumberPrinter task = new NumberPrinter();
Thread thread = new Thread(task);
thread.start();
try {
thread.join(); // Wait for the thread to complete
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Execution Strategy 2: Using ExecutorService
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecutorServiceExample {
public static void main(String[] args) {
NumberPrinter task = new NumberPrinter();
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(task); // Execute the task
executor.shutdown(); // Shutdown the executor
try {
if (!executor.awaitTermination(8000, TimeUnit.MILLISECONDS)) {
executor.shutdownNow(); // Force shutdown if task didn't finish
}
} catch (InterruptedException e) {
executor.shutdownNow(); // Force shutdown on interrupt
}
}
}
Explanation
- Versatility of
Runnable
: As shown, the sameNumberPrinter
task is executed in two different ways without any change to its implementation. This demonstrates the flexibility of usingRunnable
; the task doesn’t need to know about its execution mechanism. - Direct
Thread
Usage: The first strategy directly uses aThread
to execute theRunnable
task. This approach is straightforward and gives you direct control over thread management, such as starting and joining the thread. - Using
ExecutorService
: The second strategy uses anExecutorService
to manage task execution. This is part of Java’s Concurrency API and provides a higher-level abstraction for executing concurrent tasks. It handles thread management for you, including pooling and lifecycle management, and can be more efficient and flexible for running multiple tasks.
This example illustrates how implementing tasks as Runnable
allows you to separate the definition of the task from its execution mechanism, enabling you to choose or change how the task is executed without modifying the task itself. This design provides significant flexibility, making your code more reusable and adaptable to different execution contexts.