How to Detect Completion of Thread Pool Tasks in Java
This article explains four practical ways to determine when tasks submitted to a Java ExecutorService have finished—using Future's isDone/get, ExecutorService.invokeAll, CompletionService, and CountDownLatch—each with code examples and usage guidance.
In multithreaded Java applications, a thread pool (via ExecutorService) reuses threads to reduce the overhead of creating and destroying them. Frequently you need to know whether the tasks submitted to the pool have completed. The article presents four common techniques for this purpose.
1. Using Future
When a task is submitted to an ExecutorService, it returns a Future object. The Future provides isDone() to query completion status and get() to retrieve the result (blocking if the task is not finished).
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class FutureExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<Integer> task = () -> {
Thread.sleep(2000); // simulate work
return 42; // result
};
Future<Integer> future = executor.submit(task);
try {
if (!future.isDone()) {
System.out.println("Task not finished yet...");
}
Integer result = future.get(); // blocks until done
System.out.println("Task result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}2. Using invokeAll()
The ExecutorService.invokeAll() method accepts a collection of Callable tasks and returns a List<Future<T>>. You can iterate over the futures, checking each with isDone() and retrieving results.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class InvokeAllExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
List<Callable<Integer>> tasks = new ArrayList<>();
for (int i = 0; i < 3; i++) {
final int taskId = i;
tasks.add(() -> {
Thread.sleep(1000 * (taskId + 1)); // simulate work
return taskId;
});
}
try {
List<Future<Integer>> results = executor.invokeAll(tasks);
for (Future<Integer> future : results) {
if (future.isDone()) {
System.out.println("Task result: " + future.get());
} else {
System.out.println("Task not finished");
}
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}3. Using CompletionService
CompletionServicecombines an Executor with a BlockingQueue to let you retrieve completed tasks as soon as they finish, without polling all futures.
import java.util.concurrent.*;
public class CompletionServiceExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
CompletionService<Integer> completionService = new ExecutorCompletionService<>(executor);
for (int i = 0; i < 3; i++) {
final int taskId = i;
completionService.submit(() -> {
Thread.sleep(1000 * (taskId + 1)); // simulate work
return taskId;
});
}
for (int i = 0; i < 3; i++) {
try {
Future<Integer> future = completionService.take(); // blocks until a task completes
System.out.println("Task result: " + future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
executor.shutdown();
}
}4. Using CountDownLatch
When you need to wait for a group of threads to finish, a CountDownLatch can be used. Each worker thread calls
countDown()</b> after completing its work, and the main thread calls <code>await()to block until the latch reaches zero.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
final int taskId = i;
executor.submit(() -> {
try {
Thread.sleep(1000 * (taskId + 1)); // simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
System.out.println("Task " + taskId + " completed");
latch.countDown(); // signal completion
}
});
}
try {
latch.await(); // wait for all tasks
System.out.println("All tasks completed!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
executor.shutdown();
}
}
}By selecting the appropriate technique— Future for single-task checks, invokeAll for batch submission, CompletionService for retrieving results as they become available, or CountDownLatch for synchronizing multiple threads—Java developers can efficiently monitor task completion and improve code readability and performance.
java1234
Former senior programmer at a Fortune Global 500 company, dedicated to sharing Java expertise. Visit Feng's site: Java Knowledge Sharing, www.java1234.com
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
