CodeGym /Corsi /JAVA 25 SELF /ExecutorService, Callable, Future: esecuzione di attività...

ExecutorService, Callable, Future: esecuzione di attività

JAVA 25 SELF
Livello 54 , Lezione 1
Disponibile

1. ExecutorService: gestiamo i thread da professionisti

Perché non conviene creare semplicemente i thread con new Thread

All’inizio del multithreading tutto sembra semplice:

Thread t = new Thread(() -> {
    // facciamo qualcosa
});
t.start();

Questo approccio funziona, ma diventa presto un peso quando le attività aumentano. Ogni chiamata a new Thread() crea un nuovo thread, e decine o centinaia di thread iniziano a sovraccaricare il sistema. Inoltre è scomodo gestirli: bisogna controllare quando finiscono, cosa fare in caso di errori, come fermarli e riutilizzarli.

Qui entra in scena ExecutorService — un dispatcher di thread intelligente. Voi gli passate le attività e lui decide con quale thread e quando eseguirle. Il risultato è tutto più veloce, stabile e senza grattacapi.

Come funziona ExecutorService

ExecutorService funziona secondo un principio semplice ma efficace.

  • All’interno ha un pool di thread — un insieme di thread di lavoro già creati (fisso o dinamico).
  • Le attività finiscono in una coda e vengono prelevate dai thread liberi.
  • Il servizio gestisce il ciclo di vita: potete attendere il completamento, arrestare correttamente il pool e liberare le risorse.

Creazione di ExecutorService

Il modo più comune è usare i metodi factory della classe Executors:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService executor = Executors.newFixedThreadPool(4); // 4 thread
  • newFixedThreadPool(N) — pool di N thread (adatto alla maggior parte dei casi).
  • newCachedThreadPool() — pool dinamico, crea thread al bisogno (attenzione: con una valanga di attività si può saturare la memoria).
  • newSingleThreadExecutor() — un solo thread (esecuzione sequenziale).

Esempio: esecuzione Runnable tramite ExecutorService

executor.submit(() -> {
    System.out.println("Ciao dal pool di thread!");
});

Dopo aver finito di usare ExecutorService, è necessario terminarlo correttamente:

executor.shutdown(); // Proibisce di aggiungere nuove attività, attende il completamento di quelle correnti

Importante: Se non chiamate shutdown(), il programma potrebbe non terminare — i thread del pool resteranno in attesa di nuove attività.

2. Runnable vs Callable: non tutte le attività sono uguali

Prima di Java 5, se volevate eseguire qualcosa in un thread, scrivevate un’implementazione dell’interfaccia Runnable. È un’attività che non restituisce nulla e non lancia eccezioni verificate.

Runnable task = () -> {
    System.out.println("Sto solo lavorando, non restituisco nulla!");
};
executor.submit(task);

Callable: attività con risultato (e con eccezioni)

A volte si vuole che l’attività non solo “faccia qualcosa”, ma restituisca un risultato — per esempio la somma di numeri, l’esito di un calcolo, dati da un server. Per questo esiste l’interfaccia Callable<T>.

import java.util.concurrent.Callable;

Callable<Integer> sumTask = () -> {
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
};
  • Il metodo call() restituisce un risultato di tipo T.
  • Il metodo call() può lanciare un’eccezione verificata.

Analogia: Runnable — «vai a lavare i piatti» (il risultato non è importante), Callable — «vai a prendere il tè e dimmi a che temperatura è» (il risultato è importante).

Esecuzione di Callable: per ottenere il risultato, usa executor.submit(...). Restituirà un oggetto Future<T>.

3. Future: una promessa di risultato

Future è una “promessa” di restituire un risultato in futuro. Quando inviate un’attività a ExecutorService, ottenete un Future, dal quale potrete in seguito ottenere il risultato, sapere se l’attività è terminata o annullarla.

Metodi principali di Future

  • T get() — ottenere il risultato (attende finché l’attività non termina).
  • boolean isDone() — se l’attività è terminata.
  • boolean cancel(boolean mayInterruptIfRunning) — provare ad annullare l’attività.
  • boolean isCancelled() — se l’attività è stata annullata.

Esempio: esecuzione di Callable e ottenimento del risultato

import java.util.concurrent.*;

public class ParallelSumApp {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Callable<Integer> sumTask = () -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            return sum;
        };

        Future<Integer> future = executor.submit(sumTask);

        System.out.println("Attività avviata, puoi fare qualcos'altro...");

        // Otteniamo il risultato (il metodo blocca il thread se l'attività non è ancora terminata)
        Integer result = future.get();
        System.out.println("Risultato dei calcoli: " + result);

        executor.shutdown();
    }
}
  • L’attività viene inviata al pool di thread.
  • Finché l’attività è in esecuzione, il thread principale può fare altro.
  • Quando serve il risultato, chiamiamo future.get() — il thread attenderà se l’attività è ancora in corso.
  • Non appena l’attività termina, viene restituito il risultato.

4. Pratica: più attività e attesa del completamento

Spesso è necessario avviare più attività contemporaneamente e attendere che finiscano tutte. Per esempio, si elabora un array di dati, lo si suddivide in parti e si calcola la somma di ciascuna parte in un’attività separata.

Esempio: somma degli elementi dell’array a blocchi

import java.util.*;
import java.util.concurrent.*;

public class ParallelArraySum {
    public static void main(String[] args) throws Exception {
        int[] array = new int[1000];
        Arrays.setAll(array, i -> i + 1); // Riempiamo con i numeri da 1 a 1000

        ExecutorService executor = Executors.newFixedThreadPool(4);

        int chunkSize = array.length / 4;
        List<Future<Integer>> futures = new ArrayList<>();

        for (int i = 0; i < 4; i++) {
            int from = i * chunkSize;
            int to = (i == 3) ? array.length : (i + 1) * chunkSize;

            Callable<Integer> sumTask = () -> {
                int sum = 0;
                for (int j = from; j < to; j++) sum += array[j];
                System.out.println("Somma da " + from + " a " + (to - 1) + " = " + sum);
                return sum;
            };

            futures.add(executor.submit(sumTask));
        }

        int totalSum = 0;
        for (Future<Integer> f : futures) {
            totalSum += f.get(); // Attendiamo ogni attività a turno
        }

        System.out.println("Somma totale: " + totalSum);

        executor.shutdown();
    }
}

Qui l’array è suddiviso in 4 parti. Per ciascuna parte si crea un’attività (Callable) che calcola la somma. Tutte le attività vengono inviate a ExecutorService e vengono restituiti i Future. Infine raccogliamo i risultati di tutte le attività e li sommiamo.

Nelle attività reali è comodo usare invokeAll per attendere il completamento di tutte le attività in una volta sola.

5. Gestione degli errori con Future

Quando chiamate future.get(), se l’attività è terminata con un’eccezione, questa verrà rilanciata come ExecutionException. È importante: se qualcosa va storto nell’attività, lo scoprirete solo al momento della chiamata a get().

Esempio: gestione delle eccezioni

Callable<Integer> errorTask = () -> {
    throw new IllegalArgumentException("Qualcosa è andato storto!");
};

Future<Integer> badFuture = executor.submit(errorTask);

try {
    badFuture.get();
} catch (ExecutionException e) {
    System.out.println("L'attività è terminata con un errore: " + e.getCause());
}
  • All’interno dell’attività viene lanciata un’eccezione.
  • Alla chiamata di get() viene “incapsulata” in una ExecutionException.
  • La causa reale si può ottenere tramite getCause().

6. Dettagli utili

Come annullare un’attività

Future<?> f = executor.submit(() -> {
    while (true) {
        // Lavoro infinito
        if (Thread.currentThread().isInterrupted()) {
            System.out.println("Mi hanno chiesto di terminare!");
            break;
        }
    }
});

Thread.sleep(100); // Aspettiamo un po'
f.cancel(true); // Proviamo ad annullare l'attività
  • cancel(true) prova a interrompere l’attività, se non è ancora terminata.
  • All’interno dell’attività è consigliabile controllare Thread.currentThread().isInterrupted() e terminare correttamente.

shutdown vs shutdownNow

shutdown() — arresto morbido: vieta di aggiungere nuove attività e consente a quelle correnti di terminare tranquillamente. È l’opzione più usata.

shutdownNow() — arresto forzato: prova a interrompere i thread attivi e restituisce l’elenco delle attività che non sono riuscite a partire. Usatelo con cautela.

invokeAll e invokeAny

invokeAll(Collection<Callable<T>> tasks) avvia tutte le attività fornite e attende che finiscano. Restituisce un elenco di Future.

invokeAny(Collection<Callable<T>> tasks) attende solo la prima attività completata con successo, restituisce il suo risultato e annulla le altre. Utile quando conta la prima risposta valida.

7. Errori tipici nell’uso di ExecutorService, Callable e Future

Errore n. 1: non chiudere ExecutorService. Se vi dimenticate di chiamare shutdown(), il programma può “restare appeso” dopo la fine di main, perché i thread del pool aspettano nuove attività.

Errore n. 2: attendere il risultato subito dopo l’invio dell’attività. Se subito dopo submit() chiamate get(), non avrete i vantaggi dell’asincronia — il thread aspetterà comunque. Svolgete lavoro utile in parallelo e richiedete il risultato quando vi serve davvero.

Errore n. 3: ignorare le eccezioni nelle attività. Se non gestite ExecutionException quando chiamate get(), potete perdere errori importanti che sono avvenuti nell’attività.

Errore n. 4: uso di variabili condivise e mutabili senza sincronizzazione. Se più attività operano sugli stessi dati, serve sincronizzazione o collezioni thread-safe.

Errore n. 5: creare troppi thread. Non è il caso di creare un pool con un numero di thread molto superiore ai core della CPU — può perfino rallentare l’esecuzione.

Errore n. 6: dimenticarsi di annullare le attività. Se un’attività non serve più, annullatela tramite cancel() per non sprecare risorse.

1
Compito
JAVA 25 SELF, livello 54, lezione 1
Bloccato
Invio urgente di un pacco con drone 📦
Invio urgente di un pacco con drone 📦
1
Compito
JAVA 25 SELF, livello 54, lezione 1
Bloccato
Decifrazione dell'antica profezia 🔮
Decifrazione dell'antica profezia 🔮
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION