Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Marco Faella » 20.Comunicazione tra thread e mutua esclusione


L’interfaccia Runnable

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.

Thread creati con Runnable

È 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)

Comunicazione tra thread

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.

Comunicazione tra thread

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…

Comunicazione tra thread (segue)

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…

Comunicazione tra thread (segue)

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.

Sincronizzazione tra thread

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

  • ad ogni oggetto, indipendentemente dal tipo, è associato un mutex e una corrispodente lista d’attesa;
  • la parola chiave synchronized permette di utilizzare implicitamente tali mutex.

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.

Metodi sincronizzati

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:

  • Prima di entrare nel metodo “f”, il thread corrente tenta di acquisire il mutex di “x”
    • informalmente, è come se il thread chiamasse x.mutex.lock().
  • Se il mutex è già impegnato, il thread viene messo in attesa che si liberi.
  • Quando esce dal metodo “f”, il thread rilascia il mutex di “x”
    • informalmente, è come se il thread chiamasse x.mutex.unlock().

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”.

Blocchi sincronizzati

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”.

Osservazioni

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

  • quindi, il mutex contiene un contatore, che viene incrementato ad ogni acquisizione (lock) e decrementato ad ogni rilascio (unlock);
  • il mutex risulta libero quando il contatore vale zero.

Per certi versi, un mutex rientrante è simile ad un semaforo (counting semaphore)

  • tuttavia, un semaforo può essere incrementato e decrementato da diversi thread, mentre un thread non può né acquisire né rilasciare un mutex che in quel momento risulti acquisito (una o più volte) da un altro thread.

Esercizio (esame 8/9/2009)

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.

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion