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

Learn how to use Callable and Future in Java to retrieve results from threads efficiently using ExecutorService and advanced concurrency APIs

By Updated Java + Backend
Illustration for Callable and Future in Java: Getting Results from Threads the Right Way

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.


Part of a Series

This tutorial is part of our Java Multithreading . Explore the full guide for related topics, explanations, and best practices.

View all tutorials in this series →