Skip to content
advanced Phase 9 · Java Multithreading

ExecutorService & Futures

Use thread pools, Callable, and Future for concurrent task execution.

1h
2 problems
Topic Progress 0%

ExecutorService Interface

ExecutorService Interface

ExecutorService is a high-level API for managing thread pools and asynchronous execution.

import java.util.concurrent.*;

public class ExecutorServiceDemo {
    public static void main(String[] args) {
        // Create a fixed thread pool
        ExecutorService executor = Executors.newFixedThreadPool(4);
        
        // Submit Runnable tasks
        executor.submit(() -> {
            System.out.println("Task 1 on: " + Thread.currentThread().getName());
        });
        
        executor.submit(() -> {
            System.out.println("Task 2 on: " + Thread.currentThread().getName());
        });
        
        // Always shutdown!
        executor.shutdown();
        try {
            executor.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
}

Key methods:

  • submit(Runnable/Callable) - Returns Future
  • execute(Runnable) - Fire and forget
  • invokeAll(tasks) - Wait for all
  • invokeAny(tasks) - Wait for first success
  • shutdown() - Stop accepting new tasks
  • shutdownNow() - Interrupt all tasks
  • awaitTermination() - Wait for completion

Thread Pool Types

Thread Pool Types

1. Fixed Thread Pool

// Fixed number of threads
ExecutorService fixed = Executors.newFixedThreadPool(4);
// 4 threads, tasks queue when all busy

2. Cached Thread Pool

// Creates threads as needed, reuses idle threads
ExecutorService cached = Executors.newCachedThreadPool();
// No limit on threads (dangerous for large loads)
// Threads expire after 60 seconds idle

3. Single Thread Executor

// Single worker thread
ExecutorService single = Executors.newSingleThreadExecutor();
// Tasks execute sequentially in FIFO order

4. Scheduled Thread Pool

// For delayed/periodic tasks
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(4);

// Run after 2 seconds
scheduled.schedule(() -> {
    System.out.println("Delayed task");
}, 2, TimeUnit.SECONDS);

// Run every 5 seconds
scheduled.scheduleAtFixedRate(() -> {
    System.out.println("Periodic task");
}, 0, 5, TimeUnit.SECONDS);

5. Custom Thread Pool

ThreadPoolExecutor custom = new ThreadPoolExecutor(
    2,                      // core pool size
    4,                      // max pool size
    60L, TimeUnit.SECONDS,  // keep alive time
    new LinkedBlockingQueue<>(100)  // work queue
);

When to use which:

  • Fixed: CPU-bound tasks, controlled parallelism
  • Cached: Many short-lived tasks
  • Single: Sequential processing, resource constraints
  • Scheduled: Delays, periodic tasks

Callable and Future

Callable and Future

Callable is like Runnable but returns a value. Future represents the result.

import java.util.concurrent.*;

public class CallableFutureDemo {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        
        // Callable returns a value
        Callable<Integer> task = () -> {
            Thread.sleep(2000);  // Simulate work
            return 42;
        };
        
        // Submit and get Future
        Future<Integer> future = executor.submit(task);
        
        // Do other work while task runs...
        System.out.println("Doing other work...");
        
        // Get result (blocks until done)
        Integer result = future.get();  // Blocks for 2 seconds
        System.out.println("Result: " + result);  // 42
        
        executor.shutdown();
    }
}

Future methods:

Future<Integer> future = executor.submit(task);

// Check if done
if (future.isDone()) {
    Integer result = future.get();
}

// Get with timeout
try {
    Integer result = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    future.cancel(true);  // Cancel if taking too long
}

// Cancel
boolean cancelled = future.cancel(true);  // true = interrupt
boolean isCancelled = future.isCancelled();

Callable with lambda:

Callable<String> task = () -> {
    return "Hello from " + Thread.currentThread().getName();
};

Future<String> future = executor.submit(task);
System.out.println(future.get());

Common Patterns

Common ExecutorService Patterns

Pattern 1: Process List in Parallel

public <T> List<T> processInParallel(List<T> items, Function<T, T> processor) {
    ExecutorService executor = Executors.newFixedThreadPool(
        Runtime.getRuntime().availableProcessors()
    );
    
    List<Future<T>> futures = items.stream()
        .map(item -> executor.submit(() -> processor.apply(item)))
        .collect(Collectors.toList());
    
    List<T> results = new ArrayList<>();
    for (Future<T> future : futures) {
        try {
            results.add(future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    executor.shutdown();
    return results;
}

Pattern 2: Race Multiple Tasks

// Use invokeAny() - returns first successful result
List<Callable<String>> tasks = Arrays.asList(
    () -> fetchFromAPI1(),
    () -> fetchFromAPI2(),
    () -> fetchFromDatabase()
);

String result = executor.invokeAny(tasks);  // First to finish wins

Pattern 3: Batch Processing

List<List<Item>> batches = partition(items, 100);

List<Future<Void>> futures = batches.stream()
    .map(batch -> executor.submit(() -> processBatch(batch)))
    .collect(Collectors.toList());

// Wait for all batches
for (Future<Void> future : futures) {
    future.get();
}

Pattern 4: Rate Limiting

Semaphore semaphore = new Semaphore(10);  // Max 10 concurrent

for (Task task : tasks) {
    executor.submit(() -> {
        semaphore.acquire();
        try {
            process(task);
        } finally {
            semaphore.release();
        }
    });
}

Shutdown Strategies

Shutdown and awaitTermination

Graceful Shutdown:

ExecutorService executor = Executors.newFixedThreadPool(4);

// Submit tasks
for (int i = 0; i < 100; i++) {
    executor.submit(() -> doWork());
}

// Shutdown: stop accepting new tasks, let running finish
executor.shutdown();

// Wait for completion
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow();  // Force shutdown
        if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
            System.err.println("Pool did not terminate");
        }
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
    Thread.currentThread().interrupt();
}

Shutdown methods:

  • shutdown() - No new tasks, let current finish
  • shutdownNow() - Interrupt all tasks, return pending
  • isShutdown() - Check if shutdown called
  • isTerminated() - Check if all tasks done

Best practices:

  1. Always call shutdown() or shutdownNow()
  2. Use awaitTermination() to wait for completion
  3. Handle InterruptedException properly
  4. Use try-with-resources for ExecutorService
  5. Don't use Executors.newCachedThreadPool() in production

Practice Problems

0 / 2 solved
Parallel Sum Calculation

Use ExecutorService to calculate the sum of a large array in parallel. Split the array into chunks and sum each chunk in a separate thread.

Solution
```java
import java.util.concurrent.*;
import java.util.*;

public class ParallelSum {
    public static int parallelSum(int[] arr) throws Exception {
        int numThreads = Runtime.getRuntime().availableProcessors();
        ExecutorService executor = Executors.newFixedThreadPool(numThreads);
        
        int chunkSize = arr.length / numThreads;
        List<Future<Integer>> futures = new ArrayList<>();
        
        for (int i = 0; i < numThreads; i++) {
            int start = i * chunkSize;
            int end = (i == numThreads - 1) ? arr.length : start + chunkSize;
            
            final int[] chunk = Arrays.copyOfRange(arr, start, end);
            futures.add(executor.submit(() -> {
                int sum = 0;
                for (int val : chunk) sum += val;
                return sum;
            }));
        }
        
        int total = 0;
        for (Future<Integer> f : futures) {
            total += f.get();
        }
        
        executor.shutdown();
        return total;
    }
}
```
Timeout-Based Task Cancellation

Implement a method that runs a task with a timeout. If the task doesn't complete within the timeout, cancel it and return a default value.

Solution
```java
import java.util.concurrent.*;

public class TimeoutTask {
    public static <T> T runWithTimeout(Callable<T> task, T defaultValue, long timeoutMs) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        try {
            Future<T> future = executor.submit(task);
            return future.get(timeoutMs, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            return defaultValue;
        } catch (Exception e) {
            return defaultValue;
        } finally {
            executor.shutdownNow();
        }
    }
    
    // Usage
    public static void main(String[] args) {
        String result = runWithTimeout(
            () -> { Thread.sleep(5000); return "done"; },
            "timeout",
            1000  // 1 second timeout
        );
        System.out.println(result);  // "timeout"
    }
}
```

Quiz

1. What is the difference between submit() and execute()?

Question 1 options

2. What happens when you call shutdown() on ExecutorService?

Question 2 options

3. When should you use invokeAny()?

Question 3 options

Flashcards

Question

What is the difference between Callable and Runnable?

Answer

Callable returns a value and can throw exceptions. Runnable returns void and cannot throw checked exceptions.

Question

What are the three ExecutorService shutdown methods?

Answer

shutdown() - stop accepting, let finish. shutdownNow() - interrupt all. awaitTermination() - wait for completion.

Question

When should you use newCachedThreadPool()?

Answer

Only for many short-lived tasks. Not recommended for production due to unbounded thread creation.

Revision Notes

Key Takeaways

  • 1. submit() returns Future, execute() doesn't
  • 2. Always call shutdown() and awaitTermination()
  • 3. Callable returns values, Runnable doesn't
  • 4. invokeAny() is useful for redundant API calls

Interview Tips

  • Explain the difference between submit() and execute()
  • Know the shutdown sequence: shutdown() → awaitTermination() → shutdownNow()
  • Explain Future's get() with timeout pattern
  • Discuss when to use each thread pool type

Cheat Sheet

ExecutorService Cheat Sheet

Thread Pools

  • FixedThreadPool(n): n threads
  • CachedThreadPool: unlimited threads
  • SingleThreadExecutor: 1 thread
  • ScheduledThreadPool: delayed/periodic

Key Methods

  • submit(Runnable/Callable) → Future
  • execute(Runnable) → void
  • invokeAll() → wait for all
  • invokeAny() → first success

Shutdown

  • shutdown(): stop accepting
  • shutdownNow(): interrupt all
  • awaitTermination(): wait for done

Future

  • get(): blocks for result
  • get(timeout): blocks with timeout
  • cancel(true): interrupt task
  • isDone(): check completion