Metodologie di Programmazione: Lezione 13

Riccardo Silvestri

Threads

Una sequenza di passi eseguiti da un computer in ordine sequenziale, cioè uno dopo l'altro, è detta execution thread o brevemente thread. I programmi considerati finora sono eseguiti nel modello di programmazione detto single-threaded, cioè in un solo thread di esecuzione. In molte situazioni però l'uso di più thread di esecuzione è conveniente e a volte indispensabile. In un'applicazione con GUI, se le animazioni e i video fossero eseguiti nello stesso thread che gestisce l'interazione con l'utente, l'applicazione potrebbe risultare scarsamente reattiva agli input dell'utente. Un'applicazione che risponde a eventi o a comunicazioni remote, come ad esempio un server web, può usare centinaia di thread di esecuzione per rispondere ad altrettante richieste contemporanee emulando il comportamento di centinaia di server e minimizzando così i tempi di risposta. Un web browser può usare decine di thread per scaricare simultaneamente altrettante risorse (immagini, video e altro) contenute in una pagina, ammortizzando i tempi di attesa delle connessioni remote e velocizzando così la visualizzazione della pagina.

In una macchina monoprocessore le esecuzioni di più thread si intrecciano tra loro con rapidi scambi tra i rispettivi flussi di esecuzione. Ma in ogni dato istante, al più un solo thread è in esecuzione. Invece in una macchina multiprocessore o multicore, più thread possono essere in esecuzione simultaneamente. Mentre un thread è eseguito da un processore, un altro thread può essere eseguito da un altro processore. Quindi nelle moderne macchine multicore l'esecuzione dei thread più avvenire in parallelo offrendo così un ulteriore motivo per l'uso di più thread, cioè la possibilità di velocizzare le computazioni.

Purtroppo questi importanti benefici non sono, in generale, gratis. La programmazione di un'applicazione che vuole sfruttare al meglio i vantaggi offerti dall'uso di più thread può non essere affatto facile. Come avremo modo di vedere la programmazione concorrente (o multithreading), cioè la programmazione di più thread di esecuzione, può essere molto più delicata e complessa di quella di un singolo thread.

In questa prima lezione dedicata alla programmazione concorrente in Java, introduciamo solamente i concetti di base illustrati con alcuni semplici esempi. Nelle lezioni successive approfondiremo l'argomento e discuteremo esempi più significativi.

Creare thread

Quando la JVM viene lanciata c'è un unico1 thread di esecuzione il quale invoca il metodo main di una classe. Tutti i programmi visti finora sono eseguiti esclusivamente in questo thread che è detto appunto main thread. Tuttavia, durante l'esecuzione il programma può creare altri thread esplicitamente o implicitamente. Come vedremo in una prossima lezione, l'uso di una GUI comporta la creazione implicita di uno o più thread di esecuzione che non sono direttamente visibili dal main thread. Dal main thread possiamo creare esplicitamente un thread tramite la classe Thread in java.lang. Ogni oggetto Thread rappresenta uno specifico thread di esecuzione. Il programma che si vuole eseguire nel thread può essere specificato in vari modi, quello preferibile è definire il programma tramite l'interfaccia Runnable, in java.lang, e creare il thread tramite il costruttore:

Thread(Runnable target)
Crea un oggetto Thread che quando verrà fatto partire invocherà, nel nuovo thread, il metodo run di target.
L'interfaccia funzionale Runnable ha il solo metodo void run() che serve proprio a definire un metodo che esegue un programma o task in un qualche thread di esecuzione. Quindi i passi per creare un nuovo thread ed eseguirlo sono i seguenti.

  1. Creare un oggetto di tipo Thread impostando il task che deve essere eseguito ed eventualmente altre proprietà del thread.
  2. Iniziare l'esecuzione del nuovo thread invocando il metodo void start(). L'invocazione di start() ritorna immediatamente, non attende che l'esecuzione di run() nel nuovo thread abbia termine. L'esecuzione di run() avverrà in modo concorrente alla continuazione dell'esecuzione del thread che ha iniziato il nuovo thread (e di eventuali altri thread).

Vediamo un semplice esempio in cui dal main thread viene creato e iniziato un nuovo thread che stampa 50 linee di testo e allo stesso tempo anche il main thread stampa 50 linee di testo.

package mp.concur;

/** Classe per semplici esempi sui threads */
public class TestThread {
    public static void main(String[] args) {
        Runnable task = () -> {        // Task da eseguire in un nuovo thread
            for (int i = 0 ; i < 50 ; i++)
                out.println("Nuovo "+i);
        };
        Thread t = new Thread(task);   // Crea un nuovo thread
        t.start();                     // Inizia l'esecuzione del nuovo thread
        for (int i = 0 ; i < 50 ; i++)
            out.println("MAIN "+i);
    }
}

Il risultato potrebbe essere qualcosa del tipo:

Nuovo 0
Nuovo 1
Nuovo 2
Nuovo 3
Nuovo 4
Nuovo 5
Nuovo 6
Nuovo 7
Nuovo 8
Nuovo 9
Nuovo 10
Nuovo 11
Nuovo 12
Nuovo 13
Nuovo 14
Nuovo 15
Nuovo 16
MAIN 0
MAIN 1
MAIN 2
Nuovo 17
Nuovo 18
MAIN 3
MAIN 4
MAIN 5
MAIN 6
MAIN 7
MAIN 8
MAIN 9    
. . .

Come si vede i passi del thread principale e del nuovo thread si intrecciano in modo disordinato, mantenendo però l'ordinamento proprio di ogni singolo thread. Probabilmente, ad ogni esecuzione del programma si otterrà un intreccio differente. Questo perché l'ordine di esecuzione dei passi di due o più thread dipende da molti fattori tra cui le sofisticate ottimizzazioni dinamiche operate dal JIT della JVM. Quest'ultime rendono praticamente impossibile prevedere quale sarà l'ordine con cui si intrecceranno i passi dei diversi thread.

Vita di un thread

L'esecuzione di un nuovo thread inizia con l'invocazione del suo metodo start, il quale a sua volta invocherà il metodo run nel nuovo thread. Il metodo start di un thread può essere invocato una sola volta, le volte successive lancia l'eccezione IllegalThreadStateException. Quindi un thread può essere usato per eseguire un task una sola volta e non può più essere riusato. Però il task, al pari di qualsiasi programma, può fare qualsiasi cosa, come creare altri thread o rimanere in esecuzione continuativamente. Un thread termina non appena termina l'esecuzione del suo run(). E questo può accadere in qualsiasi modo, o eseguendo un return, o raggiungendo l'ultima istruzione o a causa di un'eccezione non catturata.

In una versione iniziale di Java fu introdotto il metodo stop che può essere invocato da un thread per terminare bruscamente un altro thread. Per motivi di compatibilità è ancora presente ma ormai da molto tempo il suo uso è fortemente sconsigliato, come si dice, il metodo è deprecato (deprecated). Le ragioni sono molteplici, sintetizzando si può dire che il metodo stop interrompe brutalmente un task senza la cooperazione del task stesso e questo può lasciare oggetti o risorse in uno stato inconsistente. Si pensi ad esempio, all'interruzione brutale di un task che sta effettuando una transazione bancaria o una prenotazione di un volo aereo. In generale i metodi deprecati devono essere considerati come errori di progettazione che non sarebbero mai dovuti esistere. Per ragioni di compatibilità non si possono eliminare ma nessun programma attuale dovrebbe usarli neanche nei casi in cui il loro uso può sembrare innocente.

Quindi non c'è nessun modo adeguato per forzare la terminazione di un thread. Però si può chiedere la terminazione invocando il metodo void interrupt(). L'effetto, se il thread sul quale è invocato è ancora in esecuzione, è che un flag interno booleano interrupted status è impostato a true. Il task eseguito dal thread può usare il metodo boolean isInterrupted() per sapere se c'è una richiesta di interruzione del thread ed agire di conseguenza. Ma se il task non fa attenzione al interrupted status, le richieste di interruzione non avranno effetto.

Consideriamo come esempio un thread contatore che dopo ogni secondo stampa il numero di millisecondi da quando è iniziata la sua esecuzione.

/** Aspetta che sia passato il numero di millisecondi specificato
 * @param millis  numero di millisecondi di attesa */
public static void waitFor(long millis) {
    long time = System.currentTimeMillis();
    while (System.currentTimeMillis() - time < millis) ;
}

public static void main(String[] args) {
    Thread counter = new Thread(() -> {
        long start = System.currentTimeMillis();
        while (true) {         // Ogni secondo stampa il numero di millisecondi
            waitFor(1000);     // passati dalla partenza del thread
            out.println(System.currentTimeMillis() - start);
        }
    });
    counter.start();
    waitFor(4000);
    counter.interrupt();
}

Il thread principale fa partire il thread contatore, aspetta per 4 secondi e poi chiede l'interruzione del thread. Se proviamo ad eseguire il programma,

1000
2000
3000
4000
5000
6000
7000
8000
9000
10000
. . .

ci accorgiamo che il thread contatore non termina. Affinché il thread sia effettivamente terminabile è necessario che faccia un controllo esplicito (e periodico) del interrupted status.

Thread counter = new Thread(() -> {
    long start = System.currentTimeMillis();
    while (true) {         // Ogni secondo stampa il numero di millisecondi
        waitFor(1000);     // passati dalla partenza del thread
        out.println(System.currentTimeMillis() - start);
        if (Thread.currentThread().isInterrupted()) break;
    }
    out.println("Conteggio terminato");
});
counter.start();
waitFor(4000);
counter.interrupt();

Per ottenere il Thread che è correntemente in esecuzione si usa il metodo statico static Thread currentThread(). Cioè Thread.currentThread() ritorna l'oggetto Thread del thread che sta eseguendo la detta invocazione. Provando la nuova versione del programma otteniamo,

1000
2000
3000
4000
5000
Conteggio terminato

Ora il thread contatore non appena si accorge che c'è una richiesta di interruzione termina la propria esecuzione.

L'implementazione che abbiamo dato del contatore non è soddisfacente perché spreca la maggior parte del tempo di esecuzione nelle attese. Durante le attese non fa nulla di significativo ma toglie tempo di esecuzione ad eventuali altri thread. Per casi come questi è conveniente usare il metodo statico

static void sleep(long millis) throws InterruptedException
Causa l'addormentamento del thread corrente per il numero specificato di millisecondi. Se durante l'addormentamento è richiesta l'interruzione del thread, lancia l'eccezione controllata InterruptedException.
Se un thread deve mettersi in attesa per un tempo prestabilito è bene che usi il metodo sleep perché così dà la possibilità alla JVM di dare più tempo all'esecuzione di altri thread. Inoltre non c'è più bisogno di controllare l'interrupted status, perché se c'è una richiesta di interruzione anche se è stata fatta prima di invocare sleep quest'ultimo lancerà InterruptedException.

Thread counter = new Thread(() -> {
    long start = System.currentTimeMillis();
    while (true) {            // Ogni secondo stampa il numero di millisecondi
        try {                 // passati dalla partenza del thread
            Thread.sleep(1000);
        } catch (InterruptedException e) { break; }
        out.println(System.currentTimeMillis() - start);
    }
    out.println("Conteggio terminato");
});
counter.start();
waitFor(4000);
counter.interrupt();

In ogni istante un thread si trova in uno specifico stato, dei sei stati sotto descritti, che si può ottenere con il metodo Thread.State getState()

NEW
Quando il thread è stato creato, ad esempio con new, ma non è stato ancora invocato il metodo start.
RUNNABLE
Dopo che è stato invocato il metodo start, il thread è eseguibile. Ma dipende dal sistema operativo e dalla JVM se è in esecuzione attualmente. Si trova in questo stato se è in esecuzione o non è in esecuzione ma è eseguibile, cioè pronto per essere eseguito.
BLOCKED
Quando è bloccato da una richiesta per ottenere un lock2 che è attualmente detenuto da qualche altro thread. Uscirà dal blocco quando il lock sarà rilasciato e sarà acquisito dal thread.
WAITING
Quando è in attesa che una certa condizione sia soddisfatta tramite ad esempio il metodo wait. Uscirà da questo stato quando ci sarà una notifica circa il soddisfacimento della condizione.
TIMED_WAITING
Quando è in attesa dopo l'invocazione di un metodo con un parametro di timeout, come ad esempio sleep.
TERMINATED
Quando l'esecuzione del run termina o normalmente o a causa di un'eccezione non catturata. Una volta che un thread entra in questo stato, vi rimane per sempre.

Gli stati NEW e TERMINATED sono, rispettivamente, lo stato iniziale e quello terminale, mentre gli altri sono stati intermedi. Durante la sua vita un thread può passare più volte per ognuno degli stati intermedi. Quando un thread si trova in BLOCKED, WAITING o TIMED_WAITING, è temporaneamente inattivo, non è in esecuzione e il suo consumo di risorse è minimo.

Reattività

Uno dei motivi che rendono la programmazione multithreading conveniente, se non indispensabile, è il mantenimento della reattività (responsiveness) della UI, cioè dell'interfaccia utente. Si immagini un'applicazione che su richiesta dell'utente deve eseguire un'operazione che può richiedere molto tempo (ad es. a causa di calcoli intensi o per l'accesso a risorse remote), se usa un unico thread, l'utente non può interagire con la UI mentre sta elaborando l'operazione, né può interrompere l'operazione stessa. Come minimo occorrono due thread, uno per gestire la UI e l'altro per eseguire l'operazione.

In seguito vedremo esempi riguardanti interfacce grafiche. Adesso consideriamo un semplice esempio relativo all'interfaccia testuale di un'applicazione che permette all'utente di digitare un intero n e di ottenere l'n-esimo numero di Fibonacci Fn, anche per valori di n molto grandi. Per poter calcolare anche solo F200 il tipo long non è sufficiente, occorre la precisione illimitata dei BigInteger in java.math. Ovviamente le operazioni aritmetiche con il tipo BigInteger sono significativamente più lente che con il tipo long. Quindi se chiediamo alla nostra applicazione di calcolare F100000 o peggio F1000000, queste operazioni potrebbero richiedere molto tempo. Perciò il thread principale sarà usato per gestire l'interazione con l'utente (lettura dell'input) e un altro thread sarà usato per calcolare Fn.

public static void fibonacci() {
    Scanner input = new Scanner(System.in);
    out.println("Digita n per calcolare F_n o 0 per terminare");
    Thread comp = null;
    while (true) {
        long n = input.nextLong();
        if (n <= 0) break;
        if (comp != null) comp.interrupt();
        comp = new Thread(() -> {
            BigInteger a = BigInteger.valueOf(0);
            BigInteger b = BigInteger.valueOf(1);
            for (long i = 1; i < n; i++) {
                BigInteger c = b.add(a);
                a = b;
                b = c;
                if (Thread.interrupted()) {
                    out.println("Calcolo di F_"+n+" interrotto");
                    return;
                }
            }
            out.println("F_"+n+" = "+b);
        });
        comp.start();
    }
    out.println("Fine");
}

Per semplicità il controllo sugli errori è omesso. È stato usato il metodo statico Thread.interrupted() invece di Thread.currentThread().isInterrupted(). L'unica differenza tra i due metodi è che il primo re-imposta l'interrupted status sempre a false dopo la lettura. Provando fibonacci() potremmo avere,

Digita n per calcolare F_n o 0 per terminare
100
F_100 = 354224848179261915075
200
F_200 = 280571172992510140037611932413038677189525
1000
F_1000 = 43466557686937456435688527675040625802564660517371780402481729089536555
         41794905189040387984007925516929592259308032263477520968962323987332247
         1161642996440906533187938298969649928516003704476137795166849228875
1000000
7
Calcolo di F_1000000 interrotto
F_7 = 13
0
Fine

Informazioni su thread

Avere informazioni riguardo ai thread può essere utile per tenere sotto controllo le operazioni in multithreading, per misurare le prestazioni o per il testing. Il modo più semplice di ottenere alcune di queste informazioni è direttamente dall'oggetto Thread

/** Ritorna una stringa con informazioni relative al thread specificato.
 * @param th  un thread
 * @return una stringa con informazioni relative al thread */
public static String threadInfo(Thread th) {
    ThreadGroup group = th.getThreadGroup();
    return String.format("Id %2d  Daemon %s  %-13s  Group %-10s  Name %s",
            th.getId(), (th.isDaemon() ? "Y" : "N"), th.getState(),
            (group != null ? group.getName() : "-"), th.getName());
}

Il metodo ThreadGroup getThreadGroup() ritorna il ThreadGroup del thread. Se non è specificato al momento della creazione, è quello del thread in cui il thread è stato creato. Può essere utile per determinare l'origine del thread, ad es. se è un thread del sistema o creato dall'utente. Ad ogni thread è assegnato un numero di identificazione, detto id, che si ottiene con long getId(), però quando il thread termina il suo id può essere assegnato ad un altro thread. Una proprietà importante di un thread è quella di essere o non essere un daemon thread. La JVM non termina finché c'è almeno un thread non daemon attivo, se invece sono rimasti attivi solamente thread daemon, la JVM termina immediatamente. Un thread può essere impostato come daemon tramite void setDaemon(boolean on), ma solamente prima di invocare start(). Il metodo boolean isDaemon() permette di sapere se il thread è daemon. Infine il nome di un thread si ottiene con String getName() e può essere impostato al momento della creazione con Thread(Runnable target, String name) o successivamente con void setName(String name).

Tutti i thread attualmente attivi, cioè quelli partiti e non ancora terminati, si ottengono con il metodo statico della classe Thread: Map<Thread,StackTraceElement[]> getAllStackTraces(). La mappa ritornata associa ad ogni thread attivo la sua stack trace momentanea. La stack trace è la traccia dello stack delle chiamate, cioè la pila delle chiamate di metodi e costruttori che determina l'attuale punto di esecuzione, ne vedremo un esempio fra poco. Per adesso ci limitiamo ad ottenere dalla mappa l'insieme dei thread attivi e per ognuno le informazioni su di esso.

/** @return una stringa con informazioni sui thread attualmente attivi */
public static String liveThreadInfo() {
    String s = "";
    for (Thread t : Thread.getAllStackTraces().keySet())
        s += threadInfo(t) + "\n";
    return s;
}

Provando il metodo potremmo ottenere (su un'altra macchina o sotto altre condizioni l'output può essere diverso)

Id  2  Daemon Y  WAITING        Group system      Name Reference Handler
Id  1  Daemon N  RUNNABLE       Group main        Name main
Id  4  Daemon Y  RUNNABLE       Group system      Name Signal Dispatcher
Id 10  Daemon Y  RUNNABLE       Group main        Name Monitor Ctrl-Break
Id  3  Daemon Y  WAITING        Group system      Name Finalizer

Come si vede, tra i thread attivi solamente quelli del gruppo main non sono daemon. Miglioriamo ora il metodo liveThreadInfo così che possa anche riportare le stack trace fino a una specificata profondità

/** Ritorna una stringa con informazioni sui thread attualmente attivi.
 * @param stackDepth  la massima profondità delle stack trace
 * @return una stringa con informazioni sui thread attualmente attivi */
public static String liveThreadInfo(int stackDepth) {
    String[] s = {""};    // Per aggirare il vincolo dell'effettivamente final
    Thread.getAllStackTraces().forEach((t,st) -> {
        s[0] += threadInfo(t) + "\n";
        for (int i = 0 ; i < st.length && i < stackDepth ; i++)
            s[0] += "    "+st[i]+"\n";
    });
    return s[0];
}

Provandolo con un limite 5 sulla profondità delle stack trace, otteniamo

Id  2  Daemon Y  WAITING        Group system      Name Reference Handler
    java.lang.Object.wait(Native Method)
    java.lang.Object.wait(Object.java:502)
    java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157)
Id  1  Daemon N  RUNNABLE       Group main        Name main
    java.lang.Thread.dumpThreads(Native Method)
    java.lang.Thread.getAllStackTraces(Thread.java:1603)
    test.TestThreads.liveThreadInfo(TestThreads.java:82)
    test.TestThreads.main(TestThreads.java:120)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Id  4  Daemon Y  RUNNABLE       Group system      Name Signal Dispatcher
Id 10  Daemon Y  RUNNABLE       Group main        Name Monitor Ctrl-Break
    java.net.InetAddress.<clinit>(InetAddress.java:289)
    java.net.InetSocketAddress.<init>(InetSocketAddress.java:187)
    java.net.ServerSocket.<init>(ServerSocket.java:237)
    java.net.ServerSocket.<init>(ServerSocket.java:128)
    com.intellij.rt.execution.application.AppMain$1.run(AppMain.java:89)
Id  3  Daemon Y  WAITING        Group system      Name Finalizer
    java.lang.Object.wait(Native Method)
    java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
    java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
    java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

Come c'era da aspettarsi quando è stato fatto il dump (cioè l'istantanea) dei thread attivi, il main thread stava eseguendo getAllStackTraces() che a sua volta era impegnato nell'esecuzione di dumpThreads() che evidentemente è un metodo privato di Thread. Si noti che per alcuni thread di sistema che operano a basso livello, come Signal Dispatcher, la stack trace non è disponibile.

Facciamo ora una prova creando nuovi thread e invocando liveThreadInfo(1) dal main thread e poi da un nuovo thread,

new Thread(() -> waitFor(10_000), "Nuovo 1").start();
new Thread(() -> {
    try {
        Thread.sleep(10_000);
    } catch (InterruptedException e) {}
}, "Nuovo 2").start();
out.println(liveThreadInfo(1));
new Thread(() -> out.println(liveThreadInfo(1)), "Nuovo 3").start();

potremmo ottenere

Id  4  Daemon Y  RUNNABLE       Group system      Name Signal Dispatcher
Id  1  Daemon N  RUNNABLE       Group main        Name main
    java.lang.Thread.dumpThreads(Native Method)
Id 12  Daemon N  TIMED_WAITING  Group main        Name Nuovo 2
    java.lang.Thread.sleep(Native Method)
Id 11  Daemon N  RUNNABLE       Group main        Name Nuovo 1
    java.lang.System.currentTimeMillis(Native Method)
Id  2  Daemon Y  WAITING        Group system      Name Reference Handler
    java.lang.Object.wait(Native Method)
Id  3  Daemon Y  WAITING        Group system      Name Finalizer
    java.lang.Object.wait(Native Method)
Id 10  Daemon Y  RUNNABLE       Group main        Name Monitor Ctrl-Break
    java.net.PlainSocketImpl.socketAccept(Native Method)

Id 13  Daemon N  RUNNABLE       Group main        Name Nuovo 3
    java.lang.Thread.dumpThreads(Native Method)
Id  4  Daemon Y  RUNNABLE       Group system      Name Signal Dispatcher
Id 12  Daemon N  TIMED_WAITING  Group main        Name Nuovo 2
    java.lang.Thread.sleep(Native Method)
Id 11  Daemon N  RUNNABLE       Group main        Name Nuovo 1
    test.TestThreads.waitFor(TestThreads.java:29)
Id  2  Daemon Y  WAITING        Group system      Name Reference Handler
    java.lang.Object.wait(Native Method)
Id  3  Daemon Y  WAITING        Group system      Name Finalizer
    java.lang.Object.wait(Native Method)
Id 10  Daemon Y  RUNNABLE       Group main        Name Monitor Ctrl-Break
    java.net.PlainSocketImpl.socketAccept(Native Method)
Id 14  Daemon N  RUNNABLE       Group main        Name DestroyJavaVM

Nel secondo report, cioè quello eseguito nel thread nuovo 3, manca il main thread perché in quel momento era già terminato. Però il programma non termina finché non terminano tutti i thread non daemon (nel nostro caso richiede circa 10 secondi). Si noti che il secondo report, che è stato eseguito immediatamente dopo che il main thread è terminato, ha catturato un'istantanea del thread DestroyJavaVM che è eseguito non appena il main thread termina.

Nelle prossime lezioni vedremo molti esempi significativi d'uso della programmazione multithreading o concorrente.

Esercizi

[Balance]    Scrivere un programma che dati due interi k e n, crea k thread che fanno tutti lo stesso calcolo: determinano se n è un numero primo o meno. Ogni thread quando termina stampa il suo nome, il risultato del test di primalità e il tempo in millisecondi che ha impiegato. Per il test di primalità si può usare il metodo mp.util.Utils.prime. Provare il programma con primi grandi, ad es. 1_000_000_000_000_000_003L e variando il numero k di thread. I tempi dei diversi thread sono simili? Come variano al variare di k?

[SleepOrNotSleep]    Scrivere un programma che dato un booleano s e un intero k, crea k thread di tipo contatore (come quelli sopra) e se s è true usano il metodo sleep altrimenti usano il metodo waitFor, e crea un thread come quelli dell'esercizio precedente (con input un primo grande). Provare il programma variando s e k. Che differenze si osservano? A cosa sono dovute?

[Fibonacci+]    Modificare il programma fibonacci in modo tale che possa mantenere in esecuzione fino a K thread per il calcolo di numeri di Fibonacci, dove K è un parametro fissato. Il programma dato sopra è relativo a K = 1. Quando l'utente chiede di effettuare un nuovo calcolo, il programma controlla quanti thread sono attualmente in esecuzione, se sono meno di K ne crea uno per il nuovo calcolo, altrimenti prima di creare il nuovo thread interrompe quello più vecchio in esecuzione. Per determinare se un thread è vivo (cioè non è ancora terminato) si può usare il metodo boolean isAlive().

[Monitor]    Creare un daemon thread che periodicamente, cioè ogni tot secondi, stampa un report dei thread attualmente attivi. Provarlo ad esempio nei programmi degli esercizi precedenti.

[Monitor+]    Migliorare il daemon thread dell'esercizio precedente così che possa produrre report più versatili secondo i parametri: ordine di stampa delle info dei thread (tramite Comparator<Thread>), filtro per i thread da monitorare (tramite Predicate<Thread>), profondità delle stack trace, possibilità di registrare i thread terminati e report di quest'ultimi.

[Parallel]    Per stimare le prestazioni di esecuzione concorrente e parallela (cioè con più processori in parallelo) si possono creare k thread ognuno dei quali esegue lo stesso calcolo e periodicamente, diciamo una volta ogni secondo, stampa lo stato di avanzamento del calcolo. Come calcolo si può usare il test di primalità mp.util.Utils.prime con input 1_000_000_000_007L ripetuto in un loop. Lo stato di avanzamento è il conteggio delle volte che è stato eseguito il test nell'ultimo periodo di un secondo. I thread dovrebbero essere daemon e devono partire il più possibile contemporaneamente. Il main potrebbe attendere un po' di secondi prima di terminare. Sperimentare variando il numero di thread k = 1,2,4,8,16,32. Come varia la somma dei conteggi dei k thread al variare di k? Perché all'aumentare di k tale somma prima cresce significativamente e poi rimane pressoché invariata?

19 Apr 2016


  1. Generalmente ci sono anche altri thread attivi ma sono usati esclusivamente dalla JVM.

  2. Tratteremo i lock più avanti, per adesso diciamo che servono a restringere l'accesso ad una risorsa in modo che un solo thread alla volta possa operare su di essa.