Runnable

The Runnable interface — the standard way to define a task for concurrent execution in Java.

Last reviewed: January 2025

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: Runnable is a functional interface with one method: run(). It represents a task that can be executed by a thread. It has no return value and cannot throw checked exceptions. Use it to define work that will run in a thread pool. For tasks that return a value, use Callable<V>. For tasks with a return value and exception handling, use CompletableFuture.

🔍 What Is It?

Runnable interface:

@FunctionalInterface
public interface Runnable {
    void run();
}

Callable interface (Runnable with return value):

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Key differences:

RunnableCallable<V>
Return valuevoidV
Checked exceptionsNoYes (throws Exception)
Used withThread, ExecutorServiceExecutorService
ReturnsnothingFuture<V>

Future<V>:

ExecutorService.submit(Callable) returns a Future<V>. Call future.get() to retrieve the result (blocks until done) or future.get(timeout, unit) for timeout.

🎯 Why Do We Need It?

Runnable separates the task definition from the execution mechanism. You define what to do (Runnable), and the ExecutorService decides when and how to run it (thread pool management). This separation makes code more testable and flexible.

🏗️ What Happens in Real Projects?

Common patterns:

// Spring @Async — Spring wraps your method in a Runnable
@Async
public void sendWelcomeEmail(String email) {
    emailService.send(email);  // runs in Spring's async thread pool
}

// ExecutorService with Runnable
executor.submit(() -> processOrder(orderId));

// ExecutorService with Callable (returns result)
Future<String> future = executor.submit(() -> {
    return fetchUserFromApi(userId);  // may throw Exception
});
String user = future.get(5, TimeUnit.SECONDS);

⚙️ How It Works

Thread.start() vs ExecutorService.submit():

  • →Thread.start() creates a new OS thread for each task — expensive
  • →ExecutorService.submit() reuses threads from the pool — efficient

Checked exceptions in Runnable:

Runnable.run() cannot throw checked exceptions. Options:

  1. Catch and handle inside run()
  2. Catch and wrap in RuntimeException
  3. Use Callable instead (allows throws Exception)
  4. Use CompletableFuture (handles exceptions properly)

💻 Example

Runnable and Callable Patterns — How to define and submit tasks for concurrent execution.

java
ExecutorService executor = Executors.newFixedThreadPool(4);

// Runnable — no return value, no checked exceptions
Runnable emailTask = () -> {
    try {
        emailService.send("user@example.com");
    } catch (EmailException e) {
        log.error("Failed to send email", e);
        // Must handle checked exceptions inside run()
    }
};
executor.submit(emailTask);

// Callable — returns value, can throw checked exceptions
Callable<UserProfile> fetchTask = () -> {
    return apiClient.fetchProfile(userId);  // may throw IOException
};

Future<UserProfile> future = executor.submit(fetchTask);

try {
    UserProfile profile = future.get(10, TimeUnit.SECONDS);
    processProfile(profile);
} catch (TimeoutException e) {
    future.cancel(true);  // interrupt the task
    log.error("Profile fetch timed out");
} catch (ExecutionException e) {
    // Wraps the exception thrown by the Callable
    Throwable cause = e.getCause();
    log.error("Profile fetch failed: {}", cause.getMessage());
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    log.error("Interrupted waiting for profile");
}

// Multiple Callables — invokeAll waits for all
List<Callable<Report>> reportTasks = List.of(
    () -> generateSalesReport(),
    () -> generateInventoryReport(),
    () -> generateFinancialReport()
);
List<Future<Report>> futures = executor.invokeAll(reportTasks);
List<Report> reports = futures.stream()
    .map(f -> { try { return f.get(); } catch (Exception e) { throw new RuntimeException(e); } })
    .collect(Collectors.toList());

🤔 Common Confusions

❓ When should I use Runnable vs Callable vs CompletableFuture?

Use Runnable for fire-and-forget tasks with no return value. Use Callable when you need a return value and Future.get() blocking is acceptable. Use CompletableFuture for async pipelines, non-blocking composition, and proper exception handling. In modern Java, CompletableFuture is preferred over Callable+Future for most use cases.

🚨 Common Production Issues

Future.get() blocking the calling thread — defeats the purpose of async. Use CompletableFuture for non-blocking composition.

Uncaught exceptions in Runnable swallowed by ExecutorService — exceptions thrown from Runnable.run() are caught by the executor and not propagated. Use Future.get() to check for exceptions, or set an UncaughtExceptionHandler.

✅ Best Practices

Prefer CompletableFuture over Callable+Future for new code.

Use @Async in Spring for simple fire-and-forget async methods.

Always handle InterruptedException and restore the interrupt flag.

Set meaningful thread names in ExecutorService for debugging.

🏛️ Architect's Note

Senior Architect's Perspective

Runnable and Callable are low-level primitives. In Spring Boot, you rarely use them directly. Use @Async for fire-and-forget, CompletableFuture for async with result, and Spring Batch for batch processing. These abstractions handle thread pool management, exception handling, and monitoring for you.

💬 Frequently Asked Questions

What is the difference between submit() and execute() in ExecutorService?

execute(Runnable) submits a task and returns void — no way to check for exceptions or completion. submit(Runnable) and submit(Callable) return a Future — you can check for completion, get the result, and handle exceptions. Always use submit() unless you genuinely do not care about the result or exceptions.

🎯 Key Takeaways

1Runnable: void return, no checked exceptions; Callable: return value, throws Exception
2ExecutorService.submit() returns Future for result/exception tracking
3Future.get() blocks — use CompletableFuture for non-blocking async
4Exceptions in Runnable are swallowed unless you call Future.get()
5In Spring Boot, use @Async instead of raw Runnable/ExecutorService

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.