In Java, l’uso di ExecutorService rappresenta una soluzione efficiente per gestire thread multipli. Tuttavia, una delle esigenze più comuni nello sviluppo multithread è sapere come attendere il completamento di tutti i task. Ecco come usare Java ExecutorService per aspettare che tutti i task siano completati, con esempi pratici e ottimizzati per la produzione.

Cos’è ExecutorService in Java
ExecutorService è un’interfaccia del pacchetto java.util.concurrent. Permette di gestire l’esecuzione di task asincroni attraverso un pool di thread. Offre metodi per sottomettere task e per controllare l’esecuzione, la terminazione e la gestione dei risultati.
- Java, Desktop Telematico e Windows 11: Una Guida per Superare l’Ostacolo dell’Installazione
- Risolvere gli errori Java su Desktop Telematico
- Come installare Java per desktop telematico
Metodi per Aspettare il Completamento di Tutti i Task
Esistono più approcci per far sì che il tuo codice attenda il completamento di tutti i task eseguiti tramite un ExecutorService.
1. Usare shutdown() e awaitTermination()
È il metodo più semplice per attendere la fine dell’esecuzione di tutti i task.
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
// Task da eseguire
System.out.println("Task eseguito da: " + Thread.currentThread().getName());
});
}
executor.shutdown(); // Rifiuta nuovi task
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // Forza la terminazione se i task non finiscono
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
Pro:
- Semplice da implementare
- Ideale per task che non restituiscono risultati
Contro:
- Non consente di raccogliere i risultati dei task
2. Usare invokeAll() per Ottenere i Risultati e Aspettare
Se hai bisogno dei risultati da più Callable, invokeAll() è la soluzione giusta. Aspetta automaticamente che tutti i task siano completati prima di restituire i risultati.
ExecutorService executor = Executors.newFixedThreadPool(5);
List<Callable<String>> tasks = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int index = i;
tasks.add(() -> "Risultato task " + index);
}
try {
List<Future<String>> futures = executor.invokeAll(tasks);
for (Future<String> future : futures) {
System.out.println(future.get());
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
Pro:
- Blocca fino a quando tutti i task sono completati
- Permette di raccogliere i risultati facilmente
Contro:
- Non adatto per
Runnable(usaCallable)

3. Usare CompletionService per Gestire Task Completati Singolarmente
Se vuoi elaborare ogni task man mano che viene completato, puoi usare CompletionService.
ExecutorService executor = Executors.newFixedThreadPool(5);
CompletionService<String> completionService = new ExecutorCompletionService<>(executor);
for (int i = 0; i < 10; i++) {
int index = i;
completionService.submit(() -> "Completato: " + index);
}
for (int i = 0; i < 10; i++) {
try {
Future<String> result = completionService.take(); // Blocca finché un task non è completato
System.out.println(result.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
executor.shutdown();
Pro:
- Ottimo per task lunghi e asincroni
- Risultati disponibili non appena ogni task termina
Contro:
- Richiede una gestione più complessa
4. Soluzione con CountDownLatch
Per una gestione manuale della sincronizzazione, CountDownLatch può essere una scelta efficace.
ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(10); // Numero di task
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
try {
// Simula lavoro
Thread.sleep(1000);
System.out.println("Task completato da: " + Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // Decrementa il contatore
}
});
}
try {
latch.await(); // Aspetta che il contatore arrivi a zero
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
executor.shutdown();
Pro:
- Pieno controllo sul completamento
- Funziona anche con
Runnable
Contro:
- Più codice da gestire
Best Practices per Aspettare il Completamento dei Task
- Evita il polling attivo: non usare cicli infiniti per verificare se i task sono terminati.
- Imposta timeout: usa
awaitTermination()con un timeout per evitare attese infinite. - Gestisci le eccezioni: usa blocchi
try-catchper prevenire errori inattesi. - Chiudi sempre il pool: invoca
shutdown()oshutdownNow()per liberare risorse.
Differenze tra shutdown(), shutdownNow() e awaitTermination()
| Metodo | Comportamento |
|---|---|
shutdown() | Interrompe l’accettazione di nuovi task. I task attivi continuano. |
shutdownNow() | Prova a interrompere tutti i task in esecuzione. |
awaitTermination() | Attende il completamento dei task entro un timeout specificato. |
Quando Usare Ogni Metodo
- Usa
invokeAll()se hai bisogno dei risultati di tutti i task. - Usa
awaitTermination()se ti interessa solo che tutti i task finiscano. - Usa
CompletionServiceper task asincroni con risposta immediata. - Usa
CountDownLatchquando vuoi gestire la sincronizzazione in modo personalizzato.
Conclusione
Sapere come usare ExecutorService per attendere il completamento di tutti i task in Java è fondamentale nello sviluppo di applicazioni concorrenti. Ogni metodo presentato ha i suoi vantaggi. La scelta dipende dalla natura dei task e dalla logica della tua applicazione.
Mantieni sempre un controllo rigoroso delle risorse, gestisci le eccezioni e assicurati di chiudere il ExecutorService per evitare memory leak o blocchi.