Skip to content
advancedPhase 17 · Java Multithreading

ExecutorService & Futures

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

1h
2 problems
Topic Progress0%

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/2solved
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

4. What is the primary purpose of ExecutorService and Thread Pools?

Question 4 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.

Question

What is ExecutorService and Thread Pools?

Answer

ExecutorService and Thread Pools is a key concept in Java programming.

Question

When to use ExecutorService and Thread Pools?

Answer

Use ExecutorService and Thread Pools when building production systems that require reliability, scalability, and maintainability.

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