I Thread e concorrenza

Lezione Concurrency (The Java Tutorials > Essential Classes) per vedere le due modalità di creazione dei thread.

Nel modello classico di programmazione, c'è un'unica central processor unit che legge le istruzioni da memoria e le esegue una dopo l'altra. Lo scopo di un programma è di provvedere una sequenza di istruzioni al processore per essere eseguito. Questo è l'unica tipo di programmazione che abbiamo considerato finora.

Tuttavia, questo modello di programmazione ha delle limitazioni. I moderni computer hanno molti processori, che permettono loro di compiere più task (lavori) contemporaneamente. Per utilizzare tutte la potenzialità di tutti questi processori, abbiamo bisogno di scrivere programmi che possono fare esecuzioni parallele (parallel processing). Per i programmatori java, questo significa imparare i thread. Un singolo thread è simile a un programma che finora abbiamo scritto, ma più di un thread possono essere eseguiti al medesimo momento, in "parallelo". Questo rende le cose più interessanti ma anche più difficili che la programmazione single thread (quella fatta finora) è che i thread in un programma sono raramente completamente indipendenti uno dall'altro. Essi in genere devono coordinarsi e comunicare tra loro.

Creazione ed esecuzione dei thread

In Java, è rappresentato da un oggetto appartenente alla classe java.lang.Thread (o una sottoclasse di questa classe). Lo scopo di un oggetto di tipo Thread è di eseguire un singolo metodo e una sola volta. Questo metodo rappresenta il compito che deve essere eseguito dal thread. Il metodo è eseguito dal proprio flusso di esecuzione (thread of control), che può essere eseguito in parallelo con gli altri thread. Quando l'esecuzione del metodo del thread è terminato o perché il metodo è finito normalmente o a causa di un'eccezione non catturata, il thread finisce di essere attivo. Quando questo succede, non c'è modo per restartare il thread o di utilizzare lo stasso oggetto di tipo Thread per far partire un altro thread.

Ci sono due modi per programmare un thread. Uno è di creare una sottoclasse di Thread e definire il metodo public void run() nella sottoclasse. Questo metodo run() definisce il compito che verrà eseguito dal thread. Cioè, quando il thread è fatto partire, è il metodo run() che verrà eseguito nel thread. Per esempio, qui c'è una semplice, ma piuttosto inutile, classe che definisce un thread che non fa altro che stampare un messaggio in standard output:

public class NamedThread extends Thread {
    private String name; // The name of this thread.
    
    public NamedThread(String name) { // Constructor gives name to thread.
        this.name = name;
    }
    
    public void run() { // The run method prints a message to standard output.
        System.out.println("Greetings from thread ’" + name + "’!");
    }
}

Per utilizzare NamedThread, bisogna chiaramente creare un oggetto appartenente a questa classe. Per esempio,

NamedThread greetings = new NamedThread("Fred");

Tuttavia, la semplice creazione dell'oggetto non manda in automatico l'esecuzione del thread o causa l'esecuzione del metodo run(). Per fare questo, è necessario invocare il metodo start() dell'oggetto di tipo thread.

In questo esempio, viene fatto con lo statement:

greetings.start();

Lo scopo del metodo start() è creare un nuovo flusso di esecuzione che eseguirà il metodo run() dell'oggetto di tipo Thread. Questo nuovo thread verrà eseguito in parallelo con il thread nel quale il metodo start() è stato chiamato, insieme con gli altri thread che già esistevano. Il metodo start() ritorna immediatamente avendo attivato il nuovo thread, senza aspettare che il thread abbia terminato. Questo significa che il codice nel metodo run() del thread è eseguito in parallelo del codice che segue la chiamata al metodo start(). Consideriamo il seguente codice:

NamedThread greetings = new NamedThread("Fred");
greetings.start();
System.out.println("Thread has been started");

Dopo che greetings.start() è eseguito, ci sono due thread. Uno che scriverà "Thread has been started" mentre l'altro vuole scrivere "Greetings from thread 'Fred'!". E' importante notare che questi messaggi possono essere stampati in entrambi gli ordini. I due thread sono eseguiti simultaneamente e sono in competizione per l'accesso allo standard output, in modo da poter scrivere i propri messaggi. A quale dei due processi capita di essere il primo ad accedere, sarà il primo a scrivere il messaggio. In un normale programma single-thread, le cose accadono in modo definito, prevedibile dall'inizio alla fine. In un programma multi-thread, c'è fondamentalmente indeterminatezza. Noi non sappiamo con certezza in che ordine accadranno le cose. Questa indeterminatezza e quello che rende la programmazione parallela cosi difficile!

Da notare che chiamare greetings.start() è molto diverso da chiamare greetings.run(). Chiamare greetings.run() eseguirà il metodo run() nello stesso thread, piuttosto che creare un nuovo thread. Questo significa che tutto il lavoro del metodo run() verrà fatto prima che il computer si muova sugli statement che seguono la chiamata a greetings.run(). Non c'è parallelismo nè indeterminatezza.

Questa discussione assume che il computer su cui si stanno eseguendo i programmi abbia più di una CPU, in modo che si possibile che sia il thread originario che il nuovo thread creato siano effettivamente eseguiti in parallelo. Tuttavia, è possibile creare molti thread anche su computer che hanno solo un processore (e, più in generale, è possibile creare molti più thread di quanti siano i processori su un computer). In questo caso, i due thread competeranno nell'utilizzo dell'unica CPU. Tuttavia, c'è ancora indeterminatezza, in quanto il processore può cambiare dall'esecuzione di uno all'altro in modo imprevedibile. In effetti, dal punto di vista del programmatore, non c'è differenza se si programma su un computer mono-processore o multi-processore, e quindi fondamentalmente ignoreremo questa distinzione d'ora in avanti.

***********************

Abbiamo detto che ci sono due modi per programmare un thread. Il primo modo è definire una sottoclasse di Thread. Il secondo è definire una classe che implementi l'interfaccia java.lang.Runnable. L'interfaccia Runnable definisce un singolo metodo, public void run(). Dato un Runnable, è possibile creare un Thread il cui compito è eseguire il metodo run() di Runnable.

La classe Thread ha un costruttore che prende un Runnable come parametro. Quando un oggetto che implementa l'interfaccia Runnable è passato al costruttore, il metodo run() del thread semplicemente invocherà il metodo run() del Runnable, e l'invocazione del metodo start() del thread, creerà un nuovo flusso di esecuzione (thread) in cui il metodo run() del Runnable è eseguito.

Per esempio, in alternativa alla classe NamedThead, protremmo definire la classe:

public class NamedRunnable implements Runnable {
  private String name; // The name of this Runnable.

  public NamedRunnable(String name) { // Constructor gives name to object.
    this.name = name;
  }
  
  public void run() { // The run method prints a message to standard output.
    System.out.println("Greetings from runnable ’" + name +"’!");
  }
}

Per utilizzare questa versione della classe, possiamo creare un oggetto di tipo NamedRunnable e utilizzare questo oggetto per creare un oggetto di tipo Thread:

NamedRunnable greetings = new NamedRunnable("Fred");
Thread greetingsThread = new Thread(greetings);
greetingsThread.start();

Il vantaggio di fare in questo modo è che ogni oggetto può implementare l'interfaccia Runnable che deve contenere il metodo run(), che verrà poi eseguito in un thread separato. Questo metodo run() ha accesso a ogni cosa nella classe, incluse le variabili e metodi private.

************

Per aiutare a comprendere come molti thread sono eseguiti in parallelo, consideriamo il semplice programma javanotes8.ThreadTest1. Questo programma crea diversi thread. Il compito è quello di contare i numeri primi che sono minori di 5000000. (Il particolare task che è eseguito non è importante per il nostro scopo. E' solo un programma demo. Sarebbe insensato avere un programma reale che abbia molti thread che eseguono lo stesso lavoro). I thread che compiono questo task sono definiti nella seguente static nested class:

    /**
     * When a thread belonging to this class is run it will count the
     * number of primes between 2 and 5000000.  It will print the result
     * to standard output, along with its id number and the elapsed
     * time between the start and the end of the computation.
     */
    private static class CountPrimesThread extends Thread {
        int id;  // An id number for this thread; specified in the constructor.
        
        public CountPrimesThread(int id) {
            this.id = id;
        }
        
        public void run() {
            long startTime = System.currentTimeMillis();
            int count = countPrimes(2, 5000000);
            long elapsedTime = System.currentTimeMillis() - startTime;
            System.out.println("Thread " + id + " counted " + 
                    count + " primes in " + (elapsedTime/1000.0) + " seconds.");
        }
    }

Il main program chiede quanti thread mandare in esecuzione, e poi crea e fa partire il numero specificati di thread:

    public static void main(String[] args) {
        int numberOfThreads = 0;
        while (numberOfThreads < 1 || numberOfThreads > 25) {
            System.out.print("How many threads do you want to use  (from 1 to 25) ?  ");
            numberOfThreads = TextIO.getlnInt();
            if (numberOfThreads < 1 || numberOfThreads > 25)
                System.out.println("Please enter a number between 1 and 25 !");
        }
        System.out.println("\nCreating " + numberOfThreads + " prime-counting threads...");
        CountPrimesThread[] worker = new CountPrimesThread[numberOfThreads];
        
        for (int i = 0; i < numberOfThreads; i++)
            worker[i] = new CountPrimesThread( i );
        
        for (int i = 0; i < numberOfThreads; i++)
            worker[i].start();
        
        System.out.println("Threads have been created and started.");
    }

Quando mando in esecuzione il programma con un solo thread, per l'esecuzione ci impiega circa 2.517 secondi. Quando mando in esecuzione il programma utilizzando otto thread, l'output è:

Creating 8 prime-counting threads...
Threads have been created and started.
Thread 0 counted 348513 primes in 8.065 seconds.
Thread 1 counted 348513 primes in 8.081 seconds.
Thread 6 counted 348513 primes in 8.029 seconds.
Thread 4 counted 348513 primes in 8.092 seconds.
Thread 2 counted 348513 primes in 8.145 seconds.
Thread 3 counted 348513 primes in 8.125 seconds.
Thread 5 counted 348513 primes in 8.122 seconds.
Thread 7 counted 348513 primes in 8.094 seconds.

Altri esempi creazione thread: Creazione di thread

Operazioni sui thread

La maggior parte delle API sui thread sono nella classe Thread della standard library. Tuttavia, iniziamo con un metodo legato ai thread della classe Runtime, una classe che permette ai programmi Java di avere informazioni rispetto all'ambiente in cui sono in esecuzione. Quando facciamo programmazione parallela allo scopo di suddividere il lavoro tra più processori, potrebbe essere utile conoscere quanti processori ha il computer su cui gira l'applicazione. In Java si può conoscere il numero dei processori chiamando la funzione:

Runtime.getRuntime().availableProcessors()

che ritorna un int con il numero dei processori che sono disponibili alla Java Virtual Machine.

**************

Un oggetto di tipo Thread ha numerosi metodi per lavorare con i thread. Il più importante è il metodo start(), che è stato visto prima.

Una volta che il thread è stato fatto partire (con il metodo start()), continuerà finché il metodo run() non terminerà per qualche motivo. Alcune volte è utile per un thread saper se un altro thread è terminato. Se thrd è un oggetto di tipo Thread, allora la funzione thrd.isAlive() ritorna un boolean che può essere utilizzato per testare se thrd è terminato o ancora "vivo" ("alive"). Un thread è vivo tra il momento in cui è fatto partire (tramite l'invocazione del metodo start()) e il momento che termina. Dopo che un thread è terminato si dice che è "morto" ("dead"). Ricordiamoci che un thread una volta che è terminato non può essere ristartato (tramite l'invocazione di start()).

Il metodo statico Thread.sleep(milliseconds) causa al thread che esegue questo metodo di andare in "sleep" ("addormentato") per il numero specificato di millisecondi. Un thread in "sleep" è ancora vivo ma non "running" (non in esecuzione). Mentre un thread è in sleeping, il computer può eseguire ogni altro thread in esecuzione (o programma in esecuzione). Thread.sleep(milliseconds) può essere utilizzato per mettere una pausa nell'esecuzione di un thread. Il metodo sleep() può lanciare un'eccezione di tipo InterruptedException, che è un'eccezione di quelle checked che è obbligatorio che sia gestita. In pratica questo significa che il metodo sleep() è invocato all'interno di try .... catch per catturare la possibile InterruptedException:

try {
    Thread.sleep(lengthOfPause);
}
catch (InterruptedException e) {
}

Un thread può mandare un interrupt a un altro thread per risvegliarlo quando è in sleep o in pausa per certe altre ragioni. Un Thread, thrd, può essere interrotto chiamando il metodo thrd.interrupt(). In questo modo si può mandare un segnale da un thread a un altro. Un thread sa di essere stato interrotto quando cattura la InterruptedException. Fuori da un blocco catch per l'eccezione, un thread può sapere se è stato interrotto chiamando il metodo statico Thread.interrupted(). Il metodo dice se il thread corrente - il thread che esegue il metodo - è stato interrotto. Esso ha anche la inusuale proprietà di pulire il flag di interrupted status del thread così che si può controllare solo una volta per l'interruzione. Molto spesso, non dobbiamo fare nulla in risposta a una InterruptedException (eccetto la catch).

A volte è necessario che un thread aspetti la fine di un altro thread. Questo è fatto con il metodo join() della classe Thread. Supponiamo che thrd è un Thread. Allora, se un thread chiama thrd.join(), allora il thread chiamante va in "sleep" finché thrd non termina. Se thrd è già terminato quando il metodo thrd.join() è chiamato, allora semplicemente non ha alcun effetto. Il metodo join() può lanciare InterruptedException, che deve essere gestita come solitamente. Come esempio, il codice seguente fa partire diversi thread, aspetta che tutti abbiano terminato, e poi stampa il tempo passato per l'esecuzione dei thread:

CountPrimesThread[] worker = new CountPrimesThread[numberOfThreads];
long startTime = System.currentTimeMillis();
for (int i = 0; i < numberOfThreads; i++) {
  worker[i] = new CountPrimesThread();
  worker[i].start();
  }

for (int i = 0; i < numberOfThreads; i++) {
  try {
    worker[i].join(); // Wait until worker[i] finishes, if it hasn’t already.
  }
  catch (InterruptedException e) {
  }
}
// At this point, all the worker threads have terminated.
long elapsedTime = System.currentTimeMillis() - startTime;
System.out.println("Total elapsed time: " + (elapsedTime/1000.0) + " seconds");

  

Un lettore attento noterà che questo codice assume che l'eccezione InterruptedException non verrà lanciata.

Per essere assolutamente sicuri che il thread worker[i] abbia terminato in un ambiente dove InterruptedException sono possibili, avremmo dovuto fare qualcosa del tipo:

while (worker[i].isAlive()) {
    try {
        worker[i].join();
    }
    catch (InterruptedException e) {
    }
}

Un'altra versione del metodo join() prende in input un parametro intero che specifica il numero massimo di millisecondi da aspettare. Una chiamata a thrd.join(m) aspetterà o fino a che il thread thrd abbia terminato o fino a che m millisecondi sono passati. Questo metodo può essere utilizzato per permettere a un thread di svegliarsi periodicamente per compiere qualche attività mentre attende. In questo frammento di codice, ad esempio, viene fatto partire un thread, thrd, e poi viene stampato un carattere '.' ogni due secondi aspettando finché thrd non termina:

System.out.print("Running the thread ");
thrd.start();
while (thrd.isAlive()) {
    try {
        thrd.join(2000);
        System.out.print(".");
    }
    catch (InterruptedException e) {
    }
}
System.out.println(" Done!");

Utilizzo della sleep per mandare in pausa il thread

Il metodo statico sleep(long) della classe Thread permette di mandare in pausa il thread che ha invocato il metodo per un certo numero di millisecondi.

Esempio in trythreads.simple.SleepMessages:

package trythreads.simple;

public class SleepMessages {
    public static void main(String args[])
        throws InterruptedException {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };
 
        for (int i = 0;
             i < importantInfo.length;
             i++) {
            //Pause for 4 seconds
            Thread.sleep(4000);
            //Print a message
            System.out.println(importantInfo[i]);
        }
    }
}

Il metodo sleep() può lanciare l'eccezione java.lang.InterruptedException, eccezione non di tipo java.lang.RuntimeException, quindi da gestire o dichiarare altrimenti nella signature del metodo che invoca Thread.sleep. Questa è un'eccezione che sleep() lancia quando un altro thread interrompe il thread che ha invocato la sleep() ed è in attesa che la sleep() termini.

In questo caso essendoci un solo thread l'InterruptedException , nessun altro thread può interrompere il thread che invoca la sleep() e quindi non è necessario gestirla ma viene dichiarata la throws nella signature del main.

Vediamo come un thread può interrompere un altro thread.

Interrupts

Un interrupt è un segnale mandato a un thread per indicargli che dovrebbe finire quello che sta facendo e quindi terminare o fare qualcos'altro. E' compito del programmatore decidere cosa fare quando è stato ricevuto un interrupt, ma è moto comune decidere di far terminare il thread.

Un thread invia un interrupt invocando il metodo interrupt() sull'oggetto di tipo Thread del thread che si vuole interrompere.

Once the thread is interrupted its interrupt status is set to true and then based on whether the thread is currently blocked or not following activity takes place:

  1. If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared (set to false again) and it will receive an java.lang.InterruptedException.

  2. If a thread that is not blocked is interrupted, then the thread’s interrupt status will be set.

Methods related to thread interruption in Java Thread class

Apart from the method interrupt() already discussed above there are two more methods in java.lang.Thread class related to thread interruption interrupted() and isInterrupted().

  • void interrupt()– Interrupts this thread.

  • static boolean interrupted()– Checks whether the current thread has been interrupted. Also clears the interrupted status of the thread.

  • boolean isInterrupted()– Tests whether this thread has been interrupted. This method doesn’t change the interrupted status of the thread in any way.

Join

This method waits until the thread on which it is called terminates. There are three overloaded versions of join() method in Java Thread class.

  • public final void join() throws InterruptedException– Waits indefinitely for this thread to die.

  • public final void join(long millis) throws InterruptedException– Waits at most the time in milliseconds for this thread to die.

  • public final void join(long millis, int nanos) throws InterruptedException– Waits at most the time in milliseconds plus additional time in nanoseconds for this thread to die

isAlive

Method, isAlive(), tests if this thread is alive. A thread is alive if it has been started and has not yet died (run() method terminated). Method returns true if thread is alive otherwise it returns false.

isAlive() method syntax

public final boolean isAlive()

Esempio utilizzo dei metodi join(), join(millis), sleep(millis) e isAlive()

Ricapitolando l'utilizzo dei metodi con l'esempio trythreads.simple.SimpleThreads:

package trythreads.simple;

public class SimpleThreads {

    // Display a message, preceded by
    // the name of the current thread
    static void threadMessage(String message) {
        String threadName =
            Thread.currentThread().getName();
        System.out.format("%s: %s%n",
                          threadName,
                          message);
    }

    private static class MessageLoop
       implements Runnable {
        
        public void run() {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            
            try {
                for (int i = 0;
                     i < importantInfo.length;
                     i++) {
                    // Pause for 4 seconds
                    Thread.sleep(4000);
                    // Print a message
                    threadMessage(importantInfo[i]);
                }
            } catch (InterruptedException e) {
                threadMessage("I wasn't done!");
            }
        }
    }

    public static void main(String args[])
        throws InterruptedException {

        // Delay, in milliseconds before
        // we interrupt MessageLoop
        // thread (default one hour).
        long  patience = 1000 * 60 * 60;

        // If command line argument
        // present, gives patience
        // in seconds.
        if (args.length > 0) {
            try {
                patience = Long.parseLong(args[0]) * 1000;
            } catch (NumberFormatException e) {
                System.err.println("Argument must be an integer.");
                System.exit(1);
            }
        }

        threadMessage("Starting MessageLoop thread");
        long startTime = System.currentTimeMillis();
        Thread t = new Thread(new MessageLoop());
        t.start();

        threadMessage("Waiting for MessageLoop thread to finish");
        // loop until MessageLoop
        // thread exits
        while (t.isAlive()) {
            threadMessage("Still waiting...");
            // Wait maximum of 1 second
            // for MessageLoop thread
            // to finish.
            t.join(1000);
            if (((System.currentTimeMillis() - startTime) > patience)
                  && t.isAlive()) {
                threadMessage("Tired of waiting!");
                t.interrupt();
                // Shouldn't be long now
                // -- wait indefinitely
                t.join();
            }
        }
        threadMessage("Finally!");
    }
}

Esempi prima parte

Esempi su utilizzo di join()

Su utilizzo di interrupt()

Pit stop

Esercizi riepilogo prima parte:

  • creazione dei thread;

  • utilizzo del metodo Thread.sleep(long timemillis);

  • utilizzo metodo join() di Thread per sincronizzarsi sulla fine di un thread;

  • utilizzo del metodo interrupt() della classe Thread;

Last updated