Nella prima parte di questa serie di tutorial dedicati alla programmazione concorrente ho illustrato in cosa consistono i thread in Java e come crearli. Vediamo ora come sfruttarli. Nel caso del web server, che abbiamo preso a titolo di esempio nella prima parte, ho spiegato che si potrebbe utilizzare un nuovo thread per ogni richiesta da servire. E’ ovvio che in questo caso i thread sono totalmente indipendenti, e non hanno bisogno di interagire.
Modifico leggermente l’esempio: supponiamo che il web server voglia contare anche il numero di pagine che ha servito; ci si trova di fronte a un problema di mutua esclusione. Questo tutorial è dedicato alla spiegazione e risoluzione di questo problema.
Il problema della mutua esclusione
Per contare le pagine servite dal nostro web server è necessario che ogni thread, dopo aver servito la richiesta per cui è stato creato, vada ad incrementare una variabile condivisa tra tutti i thread, che ha appunto la funzione di contare le richieste servite: la chiamerò countPages.
L’azione di incrementare una variabile è in realtà composta da tre istruzioni a livello macchina: la lettura del valore dalla memoria, la somma tra tale valore e 1 (cioè l’incremento vero e proprio), e la scrittura del nuovo valore in memoria. Ciò significa che questa azione non è atomica, ovvero non è indivisibile. Per spiegare meglio questo concetto utilizzo un esempio: consideriamo un thread t1 che sta eseguendo la prima delle tre istruzioni. Esso può essere interrotto dal sistema operativo, e il controllo può passare ad un altro thread t2, che magari legge a sua volta tale valore. Siamo nella condizione in cui entrambi i thread hanno letto il valore n di countPages. Ora ipotizziamo che il controllo torni al thread t1: esso esegue la somma n+1 e salva il nuovo valore nella memoria, ovvero nella variabile countPages. t1 termina e il controlla torna di nuovo a t2, che aveva letto il valore n, e quindi esegue n+1 e lo salva in countPages. Il risultato è che countPages contiene n+1, mentre dovrebbe contenere n+2, in quanto sia t1 che t2 hanno servito una richiesta.
Da qui nasce il problema della mutua esclusione: dobbiamo rendere atomica l’azione di incrementare countPages, ovvero non deve succedere che un thread t2 vada a lavorare sulla variabile countPages finchè è occupata da un thread t1.
I metodi synchronized
Fortunatamente Java ci rende la vita particolarmente semplice nel gestire questo tipo di situazioni. E’ infatti possibile specificare la keyword synchronized nell’intestazione del metodo che fornisce l’accesso alla variabile countPages (deve esserci un metodo che accede a tale variabile, non specificate public la variabile!). In particolare creiamo una classe MyCounter che rappresenta il nostro contatore, contenente il metodo increase() per incrementare la variabile countPages.
public class MyCounter {
private int countPages;
public MyCounter(){
countPages = 0;
}
public synchronized int increase(){
countPages++;
return countPages;
}
}
La Java Virtual Machine gestisce una coda per ogni oggetto che contiene metodi dichiarati synchronized. Se un thread t2 chiama il metodo increase() mentre lo sta già eseguendo un altro thread t1, esso viene inserito in coda. Quando t1 termina l’esecuzione del metodo viene prelevato il prossimo thread dalla coda, in questo caso t2. In questo modo abbiamo garantito la mutua esclusione, ovvero un solo thread per volta andrà a toccare la variabile countPages.
Modifichiamo quindi il codice della classe MyMultiThreadedProgram in modo che crei l’oggetto contatore e lo passi ai thread che vengono creati, altrimenti questi non potrebbero sapere che variabile devono incrementare:
public class MyMultiThreadedProgram {
public static void main(String args[]){
// creo il contatore
MyCounter count = new MyCounter();
// creo il mio thread
Thread t = new MyThread(count);
// attivo il thread
t.start();
}
}
Andiamo a modificare leggermente anche i nostri thread: in particolare la classe MyThread deve contenere un riferimento all’oggetto contatore:
public class MyThread extends Thread{
private MyCounter countPages;
public void run(){
// azioni del thread...
// incremento countPages
countPages.increase();
}
public MyThread(MyCounter count){
countPages = count;
}
}
Alla prossima puntata!