Attendere la Terminazione di un Thread con join in Java

Concetti Chiave
  • Il metodo join() in Java permette di attendere la terminazione di un thread specifico, garantendo che il thread chiamante aspetti fino a quando il thread specificato non è completato.
  • Il metodo isAlive() può essere utilizzato per verificare se un thread è ancora in esecuzione.
  • Utilizzare join() è una pratica comune per assicurare che il thread principale attenda la conclusione di tutti i thread figli prima di terminare.
  • Questo è particolarmente utile in scenari in cui i thread figli eseguono operazioni che devono essere completate prima della chiusura del programma.

Attendere la Terminazione di un Thread: il metodo join

Come abbiamo accennato nelle lezioni precedenti, spesso vorremo che il thread principale finisca per ultimo. In altre parole, vogliamo che il thread principale aspetti che tutti i thread figli terminino prima di fermarsi. Questo è particolarmente importante quando i thread figli stanno eseguendo operazioni che devono essere completate prima che il programma possa terminare correttamente.

Negli esempi precedenti, questo è stato realizzato chiamando sleep() all'interno di main(), con un ritardo abbastanza lungo da assicurare che tutti i thread figli terminino prima del thread principale.

Tuttavia, questa è difficilmente una soluzione soddisfacente, e solleva anche una questione più ampia: Come può un thread sapere quando un altro thread è terminato? Fortunatamente, la classe Thread fornisce un mezzo con cui possiamo rispondere a questa domanda.

Esistono due modi per determinare se un thread è terminato. Prima di tutto, possiamo chiamare isAlive() sul thread. Questo metodo è definito da Thread, e la sua forma generale è mostrata qui:

final boolean isAlive()

Il metodo isAlive() restituisce true se il thread su cui è chiamato è ancora in esecuzione. Restituisce false altrimenti.

Mentre isAlive() è occasionalmente utile, il metodo che utilizzeremo più comunemente per aspettare che un thread finisca è chiamato join(), mostrato qui:

final void join() throws InterruptedException

Questo metodo aspetta finché il thread su cui è chiamato non termina. Il suo nome deriva dal concetto del thread chiamante che aspetta finché il thread specificato non si unisce ad esso. Forme aggiuntive di join() permettono di specificare una quantità massima di tempo che vogliamo aspettare perché il thread specificato termini.

Ecco una versione migliorata dell'esempio della lezione precedente che utilizza join() per assicurare che il thread principale sia l'ultimo a fermarsi. Dimostra anche il metodo isAlive().

// Utilizzare join() per aspettare che i thread finiscano.
class NuovoThread implements Runnable {

    // Nome del thread
    String nome;
    // Il thread effettivo
    Thread t;

    NuovoThread(String nomeThread) {
        nome = nomeThread;
        t = new Thread(this, nome);
        System.out.println("Nuovo thread: " + t);
    }

    // Questo è il punto di ingresso per il thread.
    public void run() {
        try {
            for(int i = 5; i > 0; i--) {
                System.out.println(nome + ": " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println(nome + " interrotto.");
        }
        System.out.println(nome + " in uscita.");
    }
}

Di seguito è mostrato il programma principale che crea tre thread figli e li avvia, utilizzando join() per aspettare che tutti i thread finiscano prima di terminare il thread principale:

class DemostrazioneJoin {
    public static void main(String[] args) {
        NuovoThread nt1 = new NuovoThread("Uno");
        NuovoThread nt2 = new NuovoThread("Due");
        NuovoThread nt3 = new NuovoThread("Tre");

        // Avviare i thread.
        nt1.t.start();
        nt2.t.start();
        nt3.t.start();

        System.out.println("Thread Uno è vivo: "
                + nt1.t.isAlive());
        System.out.println("Thread Due è vivo: "
                + nt2.t.isAlive());
        System.out.println("Thread Tre è vivo: "
                + nt3.t.isAlive());

        // aspettare che i thread finiscano
        try {
            System.out.println("Aspettando che i thread finiscano.");
            nt1.t.join();
            nt2.t.join();
            nt3.t.join();
        } catch (InterruptedException e) {
            System.out.println("Thread principale interrotto");
        }

        System.out.println("Thread Uno è vivo: "
                + nt1.t.isAlive());
        System.out.println("Thread Due è vivo: "
                + nt2.t.isAlive());
        System.out.println("Thread Tre è vivo: "
                + nt3.t.isAlive());

        System.out.println("Thread principale in uscita.");
    }
}

L'output di esempio di questo programma è mostrato qui. (Il vostro output può variare in base all'ambiente di esecuzione specifico.)

Nuovo thread: Thread[Uno,5,main]
Nuovo thread: Thread[Due,5,main]
Nuovo thread: Thread[Tre,5,main]
Thread Uno è vivo: true
Thread Due è vivo: true
Thread Tre è vivo: true
Aspettando che i thread finiscano.
Uno: 5
Due: 5
Tre: 5
Uno: 4
Due: 4
Tre: 4
Uno: 3
Due: 3
Tre: 3
Uno: 2
Due: 2
Tre: 2
Uno: 1
Due: 1
Tre: 1
Due in uscita.
Tre in uscita.
Uno in uscita.
Thread Uno è vivo: false
Thread Due è vivo: false
Thread Tre è vivo: false
Thread principale in uscita.

Come possiamo vedere, dopo che le chiamate a join() ritornano, abbiamo la certezza che tutti i thread figli sono terminati. Inoltre, possiamo vedere che il thread principale è l'ultimo a terminare, come desiderato.