Creazione di un Thread in Java

In generale, nel linguaggio Java, si crea un thread istanziando un oggetto di tipo Thread.

Java definisce due modi in cui questo può essere ottenuto:

  • Si può realizzare una classe che implementa l'interfaccia Runnable.
  • Si può estendere la classe Thread stessa.

In questa lezione studieremo entrambe le tecniche e vedremo quando è meglio usare l'una o l'altra.

Concetti Chiave
  • In Java, si può creare un thread implementando l'interfaccia Runnable o estendendo la classe Thread.
  • Implementare Runnable è utile quando si desidera che la propria classe possa estendere un'altra classe.
  • Estendere Thread è utile quando si desidera creare un thread senza dover implementare un'interfaccia separata.
  • Il metodo run() è il punto di ingresso per un thread e deve essere implementato in entrambe le tecniche.
  • Il metodo start() avvia l'esecuzione del thread, chiamando il metodo run().

Implementare l'interfaccia Runnable

Il modo più semplice per creare un thread è creare una classe che implementa l'interfaccia Runnable.

Runnable astrae un'unità di codice eseguibile. È possibile costruire un thread su qualsiasi oggetto che implementa Runnable. Per implementare Runnable, una classe deve implementare solo un singolo metodo chiamato run(), che è dichiarato così:

public void run()

All'interno di run(), bisogna inserire il codice che costituisce il nuovo thread.

È importante capire che run() può chiamare altri metodi, usare altre classi e dichiarare variabili, proprio come può fare il thread principale. L'unica differenza è che run() stabilisce il punto di ingresso per un altro thread di esecuzione concorrente all'interno di un programma. Questo thread terminerà quando run() ritorna.

Dopo aver creato una classe che implementa Runnable, bisogna istanziare un oggetto di tipo Thread dall'interno di quella classe. Thread definisce diversi costruttori.

Quello che useremo è mostrato qui:

Thread(Runnable oggettoThread, String nomeThread)

In questo costruttore, oggettoThread è un'istanza di una classe che implementa l'interfaccia Runnable. Questo definisce dove inizierà l'esecuzione del thread. Il nome del nuovo thread è specificato da nomeThread.

Dopo che il nuovo thread è stato creato, non inizierà la propria esecuzione finché non si invoca il suo metodo start(), che è dichiarato all'interno di Thread. In sostanza, start() avvia una chiamata a run(). Il metodo start() è mostrato qui:

void start()

Ecco un esempio che crea un nuovo thread e lo avvia:

// Crea un secondo thread.
class NuovoThread implements Runnable {

    Thread t;

    NuovoThread() {
        // Crea un nuovo, secondo thread
        t = new Thread(this, "Thread Demo");
        System.out.println("Thread figlio: " + t);
    }

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

        // Questo codice viene eseguito quando il thread termina.
        System.out.println("Uscita dal thread figlio.");
    }
}
class DemoThread {
    public static void main(String[] args) {
        // crea un nuovo thread
        NuovoThread nt = new NuovoThread();
        // Avvia il thread
        nt.t.start();

        // Il thread principale continua a funzionare
        // mentre il thread figlio è in esecuzione.
        try {
            for(int i = 5; i > 0; i--) {
                System.out.println("Thread Principale: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("Thread principale interrotto.");
        }

        // Questo codice viene eseguito quando il thread principale termina.
        System.out.println("Thread principale in uscita.");
    }
}

All'interno del costruttore di NuovoThread, un nuovo oggetto Thread è creato dalla seguente istruzione:

t = new Thread(this, "Thread Demo");

Passare this come primo argomento indica che si vuole che il nuovo thread chiami il metodo run() su questo oggetto.

All'interno di main(), viene invocato start(), che avvia il thread di esecuzione iniziando dal metodo run().

Questo causa l'inizio del ciclo for del thread figlio. Successivamente il thread principale entra nel suo ciclo for. Entrambi i thread continuano a funzionare, condividendo la CPU nei sistemi single-core, finché i loro cicli finiscono. L'output prodotto da questo programma è il seguente. (L' output può variare in base all'ambiente di esecuzione specifico.)

Thread figlio: Thread[Thread Demo,5,main]
Thread Principale: 5
Thread Figlio: 5
Thread Figlio: 4
Thread Principale: 4
Thread Figlio: 3
Thread Figlio: 2
Thread Principale: 3
Thread Figlio: 1
Uscita dal thread figlio.
Thread Principale: 2
Thread Principale: 1
Thread principale in uscita.
Nota

Attesa della terminazione del thread figlio

Come menzionato in precedenza, in un programma multithreaded, è buona prassi che il thread principale sia l'ultimo thread a finire l'esecuzione.

Il programma precedente assicura che il thread principale finisca per ultimo, perché il thread principale dorme per 1.000 millisecondi tra le iterazioni, ma il thread figlio dorme per soli 500 millisecondi.

Questo causa la terminazione del thread figlio prima del thread principale. Si ricordi, tuttavia, che non è questo il modo corretto per garantire che il thread principale finisca per ultimo. Nelle prossime lezioni, vedremo come usare il metodo join() per garantire che il thread principale finisca per ultimo.

Estendere la classe Thread

Il secondo modo per creare un thread è creare una nuova classe che estende Thread, e poi creare un'istanza di quella classe.

La classe che estende deve sovrascrivere il metodo run(), che è il punto di ingresso per il nuovo thread. Come nel caso precedente, una chiamata a start() inizia l'esecuzione del nuovo thread.

Ecco il programma precedente riscritto per estendere Thread:

// Crea un secondo thread estendendo Thread
class NuovoThread extends Thread {

    NuovoThread() {
        // Crea un nuovo, secondo thread
        super("Thread Demo");
        System.out.println("Thread figlio: " + this);
    }

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

        // Questo codice viene eseguito quando il thread termina.
        System.out.println("Uscita dal thread figlio.");
    }

}
class EstendiThread {

    public static void main(String[] args) {
        // Crea un nuovo thread
        NuovoThread nt = new NuovoThread();
        // Avvia il thread figlio
        nt.start();

        // Il thread principale continua a funzionare
        // mentre il thread figlio è in esecuzione.
        try {
            for(int i = 5; i > 0; i--) {
                System.out.println("Thread Principale: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("Thread principale interrotto.");
        }

        // Questo codice viene eseguito quando il thread principale termina.
        System.out.println("Thread principale in uscita.");
    }
}

Questo programma genera lo stesso output della versione precedente.

Come si può vedere, il thread figlio viene creato istanziando un oggetto di tipo NuovoThread, che è derivato da Thread.

Si noti la chiamata a super() all'interno di NuovoThread. Questa invoca la seguente forma del costruttore Thread:

public Thread(String *nomeThread*)

Qui, nomeThread specifica il nome del thread.

Quando usare Runnable o estendere Thread

A questo punto, ci si potrebbe chiedere perché Java ha due modi per creare un thread e, soprattutto, quale sia l'approccio migliore.

La risposta alla prima domanda è che in Java non è possibile avere l'ereditarietà multipla. Ciò significa che una classe può estendere solo una singola classe. Quindi, se si desidera creare un thread, ma la propria classe deve anche estendere un'altra classe, si deve implementare Runnable e passare l'istanza di quella classe al costruttore di Thread.

La classe Thread definisce diversi metodi che possono essere sovrascritti da una classe derivata. Di questi metodi, l'unico che deve essere sovrascritto è run(). Questo è, ovviamente, lo stesso metodo richiesto quando si implementa Runnable.

Molti programmatori Java ritengono che le classi dovrebbero essere estese solo quando vengono migliorate o adattate in qualche modo. Quindi, se non si sovrascrive nessuno degli altri metodi di Thread, è probabilmente meglio implementare semplicemente Runnable.

Inoltre, implementando Runnable, la propria classe thread non ha bisogno di ereditare Thread, rendendola libera di ereditare una classe diversa.

In definitiva, quale approccio utilizzare dipende dal contesto. Tuttavia, nel resto di questa guida, creeremo thread utilizzando classi che implementano Runnable.