Nella lezione precedente, abbiamo visto come creare thread di esecuzione istanziando sottoclassi della classe Thread.
Questo metodo ha lo svantaggio che la sottoclasse di Thread che creiamo non può estendere alcuna altra classe.
In alternativa, è possibile creare un thread di esecuzione tramite una nostra classe che implementi l’interfaccia Runnable, il cui contenuto è il seguente
public interface Runnable {
public void run();
}
Come si vede, l’interfaccia contiene solo un metodo run del tutto analogo a quello della classe Thread.
Tale metodo sarà l’entry point per il nuovo thread di esecuzione.
Per creare il thread, utilizziamo il seguente costruttore della classe Thread, passandogli come argomento un oggetto di una nostra classe che implementa Runnable
public Thread(Runnable r)
Naturalmente, per far partire il nuovo thread, è sempre necessario chiamare il metodo start.
È possibile usare lo stesso oggetto Runnable per creare più thread: tutti eseguiranno lo stesso metodo run.
Quando scriviamo il metodo run di un oggetto Runnable, è facile dimenticare che non ci troviamo in una classe che estende Thread.
Quindi, non è possibile scrivere semplicemente
isInterrupted()
oppure sleep(1000)
ma bisogna scrivere
Thread.currentThread().isInterrupted()
e Thread.sleep(1000)
Piuttosto che evolvere in maniera del tutto indipendente tra loro, è spesso utile che due thread possano comunicare.
Siccome thread dello stesso processo condividono lo spazio di memoria, il modo più semplice per farli comunicare consiste nell’utilizzare degli oggetti condivisi.
Si tratta semplicemente di oggetti ai quali entrambi i thread posseggono un riferimento.
Ad esempio, supponiamo di voler creare due thread che condividano un numero intero: un thread ne modificherà il valore, mentre l’altro ne leggerà solo il contenuto.
Supponiamo di creare due classi MyThread1 e MyThread2, che estendono Thread e rappresentano le nostre due tipologie di thread.
Come fare in modo che condividano un numero intero?
Proviamo nel seguente modo:
int n = 0;
Thread t1 = new MyThread1(n);
Thread t2 = new MyThread2(n);
t1.start();
t2.start();
È possibile che i due thread, così creati, comunichino tra loro tramite la variabile “n”? La risposta è sulla slide successiva.
Naturalmente, non è possibile che i due thread della slide precedente comunichino tramite la variabile “n”.
Difatti, essendo “n” del tipo base “int”, essa verrà passata ai due costruttori per valore, e non per riferimento.
Quindi, i due costruttori riceveranno entrambi zero, e, soprattutto, due copie completamente indipendenti del valore zero.
Seppure il primo thread memorizzasse e successivamente modificasse tale valore, l’operazione non avrebbe nessun effetto sulla variabile “n”, né tantomeno sul secondo thread.
Visto che il problema è dovuto al fatto che la variabile è di un tipo base, proviamo con la seguente variante
Integer n = 0;
Thread t1 = new MyThread1(n);
Thread t2 = new MyThread2(n);
t1.start();
t2.start();
Neanche questo può funzionare, ma per un altro motivo…
Gli oggetti di tipo Integer (come tutti i tipi wrapper) sono immutabili.
Quindi, i thread non possono modificare il valore della variabile condivisa “n”.
Si noti che, se ad esempio un thread esegue
n++
questo non ha nessun effetto sull’altro thread, perché quell’istruzione è equivalente (tramite il meccanismo dell’autoboxing) a
n = new Integer(n.intValue()+1)
Ovvero, viene creato un nuovo oggetto Integer, che nulla ha a che vedere col vecchio. Dunque, come fare?
Il problema è che Java non offre una classe che contenga un intero e sia modificabile.
Dobbiamo creare noi qualcosa di simile.
Possiamo facilmente creare una classe MyInt, simile ad Integer, ma dotata di un metodo modificatore come “setValue(int m)”.
Oppure…
Possiamo usare una collezione fornita da Java, a partire da un semplice array
int[] a = new int[1];
Thread t1 = new MyThread1(a);
Thread t2 = new MyThread2(a);
t1.start();
t2.start();
Per quanto possa risultare anomalo creare un array di un solo elemento, questa soluzione è corretta e rappresenta un comodo escamotage per evitare di creare un’intera classe.
In pratica, è più comune che due thread debbano condividere un insieme di valori.
In questi casi, sarà del tutto naturale utilizzare un array, o una collezione del Java Collection Framework.
Fin qui, abbiamo ignorato i ben noti problemi di sincronizzazione che possono insorgere con l’accesso concorrente a variabili condivise.
Le prossime slide introducono l’argomento.
Se due thread tentano di modificare contemporaneamente lo stesso oggetto, l’interleaving sostanzialmente casuale può far si che l’operazione lasci l’oggetto in uno stato incoerente.
È necessario garantire che solo un thread alla volta possa modificare tale oggetto.
Questa proprietà prende il nome di mutua esclusione.
La soluzione classica al problema prevede l’uso di mutex.
Si consulti un libro sui sistemi operativi per un’introduzione ai mutex.
Un mutex è un semaforo binario che supporta le operazioni base di lock e unlock.
Java integra i mutex nel linguaggio stesso
In queste lezioni, talvolta chiameremo “x.mutex” il mutex associato all’oggetto “x”.
Questa è una notazione puramente didattica, che non trova corrispondenza nel linguaggio Java.
Il modificatore synchronized si può applicare ad un metodo, oppure ad un blocco di codice.
Si noti che non si può applicare synchronized ad un campo o altra variabile.
Consideriamo il caso di un metodo a cui sia applicato il modificatore synchronized.
In tal caso, diremo che il metodo è sincronizzato
public synchronized int f(int n) { ... }
Supponiamo che “x.f(3)” sia una chiamata a tale metodo.
L’effetto del modificatore synchronized è il seguente:
In altre parole, quando un thread chiama un metodo sincronizzato “f” di un dato oggetto, altri thread che chiamino qualunque metodo sincronizzato dello stesso oggetto devono aspettare che il primo thread esca dalla chiamata a “f”.
Questo garantisce che solo un thread alla volta possa eseguire i metodi sincronizzati di ciascun oggetto.
Se un metodo statico di una classe “A” è sincronizzato, un thread che lo voglia eseguire deve acquisire il mutex dell’oggetto Class corrispondente alla classe “A”.
La parola chiave synchronized può anche introdurre un blocco di codice.
In questo caso, parleremo di blocco (di codice) sincronizzato.
Usato in questo modo, synchronized richiede come argomento l’oggetto del quale vogliamo acquisire il mutex.
Ad esempio, il seguente frammento di codice:
Integer n = 0;
synchronized (n) {
...
}
corrisponde informalmente a (il codice seguente non è Java, ma è solo esemplificativo):
Integer n = 0;
n.mutex.lock();
...
n.mutex.unlock();
Si noti che i mutex acquisiti dai blocchi sincronizzati sono gli stessi che sono utilizzati anche dai metodi sincronizzati.
Quindi, se un thread sta eseguendo un blocco che è sincronizzato sull’oggetto “x”, gli altri thread devono aspettare per eseguire eventuali metodi sincronizzati di “x”.
I mutex impliciti di Java sono rientranti (reentrant).
Ciò vuol dire che un thread può acquisire lo stesso mutex più volte.
Questo accade comunemente, ogni qual volta un metodo sincronizzato ne chiama un altro, anch’esso sincronizzato.
Se i mutex non fossero rientranti, un metodo sincronizzato che ne chiamasse un altro andrebbe automaticamente in deadlock.
Internamente, il mutex ricorda quante volte è stato acquisito dallo stesso thread
Per certi versi, un mutex rientrante è simile ad un semaforo (counting semaphore)
La classe Auction rappresenta una vendita all’asta. Il suo costruttore accetta come argomento il prezzo di partenza dell’asta.
Il metodo makeOffer rappresenta la presentazione di un’offerta e prende come argomenti l’ammontare dell’offerta (un numero intero) e il nome dell’acquirente (una stringa).
Un oggetto Auction deve accettare offerte, finché non riceve offerte per 3 secondi consecutivi.
A quel punto, l’oggetto stampa a video l’offerta più alta ricevuta e il nome del compratore.
Si supponga che più thread possano chiamare concorrentemente il metodo makeOffer dello stesso oggetto.
Esempio d’uso:
Auction a = new Auction(1000);
a.makeOffer(1100, "Marco");
a.makeOffer(1200, "Luca");
Thread.sleep(1000);
a.makeOffer(200, "Anna");
Thread.sleep(1000);
a.makeOffer(1500, "Giulia");
Thread.sleep(4000);
Output dell’esempio d’uso:
Oggetto venduto a Giulia per 1500 euro.
4. Risoluzione dell'overloading e dell'overriding
5. Controllo di uguaglianza tra oggetti
6. Classi interne, locali ed anonime
7. Iteratori, teoria e pratica
8. Clonazione di oggetti. Confronto tra oggetti.
9. Elementi di programmazione di interfacce grafiche
10. Il paradigma Model-View-Controller. Il pattern Strategy
11. I pattern Composite e Decorator
12. I pattern Template Method e Factory Method
13. Classi e metodi parametrici
14. La libreria Java Collection Framework: le interfacce Iterable, ...
15. La libreria Java Collection Framework: la classe HashSet e le l...
16. Parametri di tipo con limiti
17. L'implementazione della programmazione generica: la cancellazio...
18. La riflessione
19. Introduzione al multi-threading
22. Classi enumerate