Introduzione
L’utilizzo di ExecutorService in Java è una pratica comune per la gestione di thread e task asincroni. Quando si avviano più task simultaneamente, è fondamentale sapere come verificare se tutti i task sono completati. In questa guida, esploreremo in dettaglio come usare ExecutorService per controllare lo stato dei task, quali metodi sono più efficaci e come gestire il flusso in modo sicuro e performante.

Cos’è Java ExecutorService
ExecutorService fa parte del pacchetto java.util.concurrent. È una delle API principali per l’esecuzione di task concorrenti. Permette di astrarre la gestione dei thread e offre metodi come submit(), invokeAll() e shutdown().
Esecuzione di più task con ExecutorService
Ecco un esempio semplice di come lanciare più task in parallelo usando ExecutorService:
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int taskId = i;
Callable<String> task = () -> {
Thread.sleep(1000);
return "Task " + taskId + " completato";
};
futures.add(executor.submit(task));
}
Come verificare se tutti i task sono completati
Una delle domande più comuni è: come controllare se tutti i Future sono completati?
Ecco alcuni approcci pratici.
1. Controllare ogni Future con isDone()
Ogni oggetto Future ha il metodo isDone(). Puoi verificare lo stato di ogni task iterando sulla lista:
boolean allDone = futures.stream().allMatch(Future::isDone);
Questo codice restituisce true solo quando tutti i task sono completati.
Puoi anche usare un ciclo per controllare periodicamente:
while (true) {
boolean allDone = true;
for (Future<?> future : futures) {
if (!future.isDone()) {
allDone = false;
break;
}
}
if (allDone) {
System.out.println("Tutti i task sono stati completati.");
break;
}
Thread.sleep(500); // Polling interval
}
Questo approccio è semplice ma efficace per controllare l’avanzamento.
2. Usare invokeAll() per attendere il completamento
Un’alternativa pulita è usare invokeAll(), che blocca finché tutti i task non sono completati:
List<Callable<String>> tasks = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int taskId = i;
tasks.add(() -> {
Thread.sleep(1000);
return "Task " + taskId + " completato";
});
}
List<Future<String>> results = executor.invokeAll(tasks);
for (Future<String> result : results) {
System.out.println(result.get());
}
Con invokeAll(), non serve controllare ogni Future, poiché il metodo restituisce solo al termine di tutti i task.

3. Usare CompletionService per monitorare i task completati
Se vuoi elaborare i risultati appena un task termina, puoi usare ExecutorCompletionService:
ExecutorService executor = Executors.newFixedThreadPool(5);
CompletionService<String> completionService = new ExecutorCompletionService<>(executor);
for (int i = 0; i < 10; i++) {
int taskId = i;
completionService.submit(() -> {
Thread.sleep(1000);
return "Task " + taskId + " completato";
});
}
for (int i = 0; i < 10; i++) {
Future<String> result = completionService.take(); // blocca finché un task è completato
System.out.println(result.get());
}
Questo metodo è utile per processare i risultati nell’ordine in cui i task finiscono, senza attese inutili.
4. Shutdown e awaitTermination
Una volta inviati tutti i task, puoi chiudere l’ExecutorService e attendere la sua terminazione:
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
if (executor.isTerminated()) {
System.out.println("Tutti i task sono completati.");
}
Questa combinazione è utile per task di lunga durata o per evitare di lasciare thread pendenti.
5. Esempio completo con controllo dello stato dei task
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
int id = i;
futures.add(executor.submit(() -> {
Thread.sleep(1500);
return "Task " + id + " finito";
}));
}
while (true) {
boolean allDone = futures.stream().allMatch(Future::isDone);
if (allDone) {
System.out.println("Tutti i task sono terminati.");
break;
}
System.out.println("In attesa del completamento...");
Thread.sleep(500);
}
for (Future<String> future : futures) {
System.out.println(future.get());
}
executor.shutdown();
Questo codice mostra un controllo continuo dello stato dei task e stampa i risultati alla fine.
Best Practices da seguire
- Non dimenticare mai di chiamare
shutdown()per evitare memory leak. - Evita il busy waiting, usa
Thread.sleep()oawaitTermination()dove possibile. - Non usare
.get()senza controllo, perché blocca il thread chiamante. - Gestisci correttamente le eccezioni, specialmente
InterruptedExceptioneExecutionException.
Conclusione
Controllare se tutti i task sono completati in ExecutorService è essenziale per una gestione efficace della concorrenza. Usando metodi come isDone(), invokeAll() e awaitTermination(), puoi garantire che i tuoi thread si comportino in modo prevedibile e sicuro. Scegli l’approccio più adatto alla tua esigenza, tenendo conto di performance e semplicità.