Callable and Future in Java: Getting Results from Threads the Right Way

Illustration for Callable and Future in Java: Getting Results from Threads the Right Way
By Last updated:

Java's Callable and Future are powerful tools for multithreaded programming, especially when you need to get a result back from a thread. Unlike Runnable, which cannot return a result or throw a checked exception, Callable enables developers to submit tasks that produce values.

This tutorial dives deep into how Callable and Future work, where they fit in the multithreading landscape, real-world use cases, performance considerations, and how to use them effectively in Java 8 through Java 21+.

1. Introduction to Multithreading

Multithreading allows concurrent execution of two or more parts of a program for maximum CPU utilization. Java supports multithreading natively, helping you write high-performance applications.

Real-world importance:

  • Parallel file processing
  • Background computations in GUIs
  • Network request handling in web servers

2. Thread Lifecycle in Java

  • NEWRUNNABLERUNNINGBLOCKEDTERMINATED
  • Methods like start(), run(), join() influence these transitions.

3. Callable vs Runnable

Feature Runnable Callable
Return value No Yes
Exception Cannot throw Can throw checked
Interface method run() call()

Example: Using Callable

Callable<Integer> task = () -> {
    return 42;
};

4. Getting Results with Future

A Future represents the result of an asynchronous computation.

Example:

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
    TimeUnit.SECONDS.sleep(1);
    return 123;
});

System.out.println("Result: " + future.get()); // blocks until result is available

5. ExecutorService and Thread Pools

Thread pools improve performance by reusing threads.

  • Executors.newFixedThreadPool(5)
  • Executors.newCachedThreadPool()
  • Executors.newScheduledThreadPool()

6. Java Memory Model and volatile

  • Ensures visibility and ordering of variables across threads.
  • Use volatile when multiple threads update a single variable.

7. Synchronization Tools

  • join(): Waits for thread completion.
  • wait() / notify(): Intrinsic locks for coordination.
  • sleep(): Pauses execution.

8. Locking Strategies

  • synchronized: Basic locking
  • ReentrantLock: More control, tryLock, fairness
  • StampedLock: For optimistic reads
  • ReadWriteLock: For high-read/low-write

9. Advanced Concurrency Classes

  • ConcurrentHashMap: High-performance thread-safe maps
  • BlockingQueue: Used in producer-consumer scenarios
  • ForkJoinPool: For divide-and-conquer parallelism
  • CompletableFuture: Async programming with pipelines

10. Real-world Use Cases

  • Producer–Consumer with BlockingQueue
  • Thread pool for web server handling
  • Batch file processing using invokeAll()

11. Best Practices and Anti-patterns

✅ Do

  • Use ExecutorService for thread reuse
  • Handle exceptions properly in Future.get()

❌ Avoid

  • Blocking the main thread unnecessarily
  • Creating threads manually for short tasks

12. Multithreading Design Patterns

  • Worker Thread: Processes tasks from a queue
  • Future Task: Wrapper for async execution
  • Thread-per-message: One thread per request

13. 📌 What’s New in Java [version]?

Java 8

  • Lambdas for Runnable
  • CompletableFuture
  • Parallel Streams

Java 9

  • Flow API (Reactive Streams)

Java 11+

  • Minor improvements in CompletableFuture
  • Cleaner APIs

Java 21

  • Virtual Threads (Project Loom)
  • Structured Concurrency
  • Scoped Values

14. Conclusion and Key Takeaways

Callable and Future simplify result-handling in multithreaded code, offering better performance, exception handling, and scalability when used with thread pools and executor frameworks.

Key Takeaways:

  • Prefer Callable when you need results
  • Always shut down the ExecutorService
  • Embrace CompletableFuture for complex pipelines
  • Use structured concurrency (Java 21+) for clarity

15. FAQ

Q1: Why not call run() directly on a thread?
A: That runs it on the current thread. Use start() to run it concurrently.

Q2: What happens if Future.get() is called before the task is done?
A: It blocks until the result is available.

Q3: What’s the difference between invokeAll() and submit()?
A: invokeAll() waits for all tasks to finish. submit() is used per-task.

Q4: Can Callable throw checked exceptions?
A: Yes! Unlike Runnable.

Q5: What if the thread pool is full?
A: Tasks are queued or rejected based on policy.

Q6: What’s false sharing in threads?
A: Cache contention due to nearby variables on same cache line.

Q7: How does CompletableFuture differ from Future?
A: It's non-blocking and supports chaining.

Q8: When should I use volatile?
A: When multiple threads read/write a simple shared variable.

Q9: Are synchronized and ReentrantLock interchangeable?
A: Mostly yes, but ReentrantLock provides more control.

Q10: Are virtual threads production-ready?
A: Yes, from Java 21 onwards with Project Loom.