Java ExecutorService: Check if All Tasks Are Done – Guida Completa

java-executorservice-check-if-all-tasks-are-done

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.

java-executorservice-check-if-all-tasks-are-done

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() o awaitTermination() dove possibile.
  • Non usare .get() senza controllo, perché blocca il thread chiamante.
  • Gestisci correttamente le eccezioni, specialmente InterruptedException e ExecutionException.

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à.

By Mario Lattice

Appassionato e sempre entusiasta della tecnologia e di poterla usare. Amo scrivere per raccontare le ultime novità tecnologiche.

Puoi leggere

No widgets found. Go to Widget page and add the widget in Offcanvas Sidebar Widget Area.