Java Tutorial - Parte 2 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Versione 0.8: le code di invio e ricezione

Introduzione

Nella sezione Il metodo sendMessage avevo accennato ai problemi derivanti nella implementazione attraverso un worker-thread della operazione di invio dei messaggi usando la stessa tecnica descritta per la operazione di connessione. Vediamo brevemente come si presenterebbe il metodo sendMessage usando la stessa soluzione:

// File: Connection.java
public void sendMessage( Message msg )
{
workerSend = new SwingWorker<Integer, Void>() {
Override
public String doInBackground() throws MMException
{
int msgID = doSendMessage( msg );
return msgId;
}
@Override
public void done()
{
try {
int msgId = get();
messageSent( msgId );
}
catch ( ... omissis ... )
};
workerSend.execute();
}

Indubbiamente, la soluzione è davvero semplice ma presenta delle criticità da non sottovalutare che possono essere riepilogate di seguito:

  • non abbiamo il controllo su quanti threads di invio saranno creati ed eseguiti; essendo un gioco basato sui turni, questo non è un grosso problema per Mastermind dal momento che il server invia un massimo di tre messaggi uno dietro l'altro: haveGuess, haveResults ed infine swapTurn; tuttavia, in applicazioni al alto traffico di rete potremmo avere un aumento incontrollato dei threads di invio
  • se un messaggio non viene trasmesso con successo, è necessario prevedere ulteriore codice che si occupa di ricreare il thread di invio che ritrasmetta il messaggio; questo problema potrebbe essere risolto nel metodo del listener connectionError ma introduce una certa complessità nel codice, proprio quello che volevamo evitare
  • poichè i threads di invio vengono eseguiti in modo asincrono uno rispetto all'altro non vi è alcuna garanzia che i messaggi saranno trasmessi nell'ordine in cui sono stati creati; potrebbe accadere che il messaggio swapTurn arrivi al client prima del messaggio notifyResults; non sarebbe nemmeno un problema grave poichè la sequenza di arrivo non è importante in Mastermind ma potrebbe accadere che il messaggio notifyResults non arrivi a causa di errori di connessione e questo lascierebbe il client in uno stato inconsistente: ha il turno di gioco senza vedere i risultati del confronto

La soluzione, elegante ed efficace, a tutti e tre i problemi si chiama: le code bloccanti. Il linguaggio Java mette a disposizione diverse implementazioni delle code bloccanti che vengono descritte dalla interfaccia BlockingQueue. La implementazione usata in mastermind è la ArrayBlockingQueue, contenuta nel package java.util.concurrent.

Le blocking queues sono code FIFO o LIFO nelle quali un elemento viene inserito solo se vi è spazio a sufficenza; se non c'è spazio, la operazione blocca il thread finchè non si libera spazio.
Di contro, un elemento viene rimosso dalla coda solo se vi è almeno un elemento disponibile; se la coda è vuota, l'operazione blocca il thread fino a quando un elemento non diventi disponibile.

I files sorgente

Le code bloccanti in mastermind sono implementate nei due files sorgente:

nome file package descrizione
SendQueue.java mastermind.net la coda bloccante di invio
RecvQueue.java mastermind.net la coda bloccante di ricezione

La coda di invio

La strategia implementata in MasterMind per risolvere i problemi descritti in introduzione si basa su questi principi i quali usano la coda bloccante:

  • la coda di invio è di tipo FIFO: i messaggi vengono inviati sul canale di comunicazione nello stesso ordine in cui sono stati inseriti in coda evitando il problema che il client possa ricevere un messaggio fuori sequenza
  • viene eseguito un thread separato che esamina la coda di invio: se essa è vuota non esegue nulla; se vi è almeno un elemento in coda NON lo preleva dalla coda ma lo esamina: l'elemento viene trasmesso sul canale di comunicazione usando il metodo bloccante doSendMessage
  • se il metodo bloccante di trasmissione ritorna regolarmente, il messaggio esaminato e correttamente trasmesso viene rimosso dalla coda
  • se il metodo bloccante di trasmissione solleva una eccezione e quindi il messaggio non è stato trasmesso, il messaggio NON viene rimosso dalla coda e quindi sarà il primo messaggio da trasmettere se e quando la connessione sarà ripristinata
  • vi è un solo thread che esamina la coda di invio: esso è un thread che non termina mai, salvo la cancellazione del thread, ovviamente

Con questa soluzione abbiamo anche un altro vantaggioche riguarda il metodo Connection.sendMessage: se il metodo ritorna, possiamo disinteressarci totalmente dell'esito della trasmissione:

  • se la trasmissione è andata a buon fine, tutto bene
  • se il messaggio non può essere trasmesso regolarmente, rimane in coda: quando e se la connessione sarà ripristinata esso sarà trasmesso

Ma se nella coda di invio non c'è più spazio? Ecco perchè si usano le code bloccanti: se non c'è più spazio, il EDT si blocca nel tentativo di inserire il messaggio in coda; poichè la trasmissione avviene in un thread separato, prima o poi un messaggio verrà trasmesso sul canale di comunicazione e un posto si libererà.
Se la connessione è talmente lenta da bloccare parzialmente la GUI, allora è meglio così: l'utente avrà così un feedback notando il forte rallentamento.

La coda di ricezione

A dire il vero, una coda di ricezione non è strettamente necessaria. Non serve per ripristinare la ricezione di un messaggio in caso di errori di connessione dal momento che, se il messaggio non è stato ricevuto correttamente esso non è nemmeno stato inviato con successo: ci penserò la coda di invio del remoto a ritrasmetterlo.
Per quanto riguarda la lentezza della connessione, essa si risolve nel worker-thread di lettura: dal momento che la operazione di lettura è bloccante, i messaggi arriveranno molto lentamente ma questo non bloccherà la GUI dal momento che la effettiva ricezione del messaggio viene eseguita in un thread separato.

Il thread SendQueue

Abbiamo già incontrato il concetto di worker-thread in Versione 0.5, introdurre un ritardo ed anche in Versione 0.8, i metodi di I/O.
Nel primo caso abbiamo usato uno SwingWorker per introdurre un ritardo nella esecuzione di una operazione che sarebbe, in effetti, immediata. Nel secondo caso, invece, abbiamo usato uno SwingWorker per eseguire operazioni di I/O bloccanti le quali, se eseguite nel EDT, potrebbero congelare la GUI.

Prendiamo come esempio il caso del giocatore A.I.: esso elabora la guess in una frazione di secondo ma, per dare al giocatore umano l'impressione che la macchina ci pensa in po su abbiamo escogitato il seguente trucchetto:

  • quando la A.I. ha elaborato la guess, essa viene fornita come argomento ad un thread separato
  • il thread separato "dorme" per un certo periodo di tempo in modo da simulare la concentrazione della A.I.
  • scaduto il tempo, il thread separato termina e viene richiamato il metodo done
  • quest'ultimo, eseguito nel thread principale (il EDT, vedi Il Event Dispatching Thread), invia la guess al server di gioco

Questo thread separato quindi ha un inizio ed una fine: quando il metodo doInBackground termina, il dato elaborato nel thread separato è pronto e può essere restituito per essere poi elaborato nel thread principale. Per la operazione di introdurre un ritardo fittizio nella elaborazione della guess da parte della A.I. questa modalità di esecuzione è perfettamente funzionale e assolutamente congrua ed efficiente: una volta inviata la guess al server, non ha più senso mantenere in esecuzione il thread separato; meglio creare un altro thread quando una guess sarà nuovamente disponibile.

Diverso è il caso in cui serve un thread separato per gestire una coda di messaggi da inviare: creare un thread per ogni messaggio da inviare sarebbe, oltre che costoso in termini di tempo e risorse, controproducente dal momento che è proprio ciò che vogliamo evitare. Meglio creare un thread unico che gestisce la intera coda: se la coda è vuota il thread può dormire per qualche centinaio di millisecondi; se invece vi è almeno un messaggio da inviare il thread lo invia e poi verifica nuovamente se la coda è vuota oppure no. Si tratta quindi di un thread che non termina mai.
Voglio attirare la attenzione del lettore sul fatto che il thread derivato da SwingWorker è del tipo one-shot (=spara una sola volta) nel senso che, una volta finito, il thread non può più essere ri-eseguito nemmeno se viene richiamato nuovamente il metodo execute.

Come facciamo allora a restituire il dato elaborato dal thread se esso deve terminare per restituirlo? La classe SwingWorker può essere eseguita in un altro modo oltre a quello visto finora: essa è una classe parametrizzata nel senso che è specializzata nel restituire un tipo di dato specifico ma, come potete osservare dalla sua dichiarazione, essa accetta due parametri (T e V) ed essi possono anche essere di tipi diversi:

public abstract class SwingWorker<T,​V>
extends Object
implements RunnableFuture<T>
Type Parameters:
T - the result type returned by this SwingWorker's doInBackground and get methods
V - the type used for carrying out intermediate results by this SwingWorker's publish
and process methods

Il primo tipo (T) è il tipo di dato restituito dal metodo doInbackground quando esso termina (e questo fà terminare anche il thread).
Il secondo tipo (V) è il tipo di dato restituito come risultato intermedio del metodo publish che, al pari di quello in background, viene eseguito nel thread secondario. Mentre il metodo doInBackground ha la sua controparte nel EDT in done, il metodo publish ha la sua controparte in EDT nel metodo process.

Possiamo quindi sfruttare questa caratteristica della classe SwingWorker per mantenere sempre in esecuzione il worker-thread che ispeziona la coda dei messaggi ed inviare i messaggi in coda come risultati intermedi. In questo scenario, il worker-thread non ha un dato da restituire e quindi non terminerà mai diventando pertanto un thread infinito; cosa restituità il thread infinito quando termina? Ovviamente null! I parametri del thread SendQueue saranno:

  • Void come primo parametro: il dato restituito alla fine del thread
  • Message come secondo parametro: il dato restituito come risultato intermedio
private class SendQueue extends SwingWorker<Void, Message>
{

Le operazioni eseguite nella classe SendQueue sono sostanzialmente queste:

  • il metodo doInBackground viene eseguito in un ciclo while infinito, salvo la cancellazione del thread, ovviamente
  • nel metodo doInBackground viene inviato un messaggio in coda, se esiste
  • nel metodo doInBackground viene pubblicata la ID del messaggio inviato, richiamando il metodo publish
  • nel EDT, il metodo process elabora la ID del messaggio inviato

SendQueue: i membri dati

// File: SendQueue.java
// la dimensione della coda, in numero di elementi
private static final int QUEUE_SIZE = 50;
// se la coda è vuota, il thread dorme per 100 millisecondi
private static final int SLEEP_TIME = 100;
// la coda bloccante, allocata nel costruttore
private static BlockingQueue<Message> queue = null;
// l'oggetto connessione collegato a questa coda
private Connection connection;

Una nota di commento merita la definizione della coda bloccante queue. Perchè dichiararla statica? Ebbene, questo è proprio il trucco che permette la ritrasmissione dei messaggi non andati a buon fine in presenza di errori di connessione. Poichè la coda di invio termina in presenza di eccezioni, si rende necessario costruire una nuova istanza della classe SendQueue se e quando la connessione sarà ripristinata.
Tuttavia, la coda esistente che contiene eventuali messaggi non trasmessi resterà in essere essendo un membro statico.

SendQueue: il costruttore

// File: SendQueue.java
public SendQueue( Connection conn )
{
if ( queue == null ) {
queue = new ArrayBlockingQueue<Message>( QUEUE_SIZE );
}
connection = conn;
}

Il costruttore della coda di invio dei messaggi alloca la coda bloccante in un membro statico: questo è necessario perchè la coda dei messaggi, pur essendo un thread separato, può terminare, per esempio a causa di errori di comunicazione.

Accodare un messaggio

Per aggiungere un messaggio alla coda di invio ho scritto il metodo addMessage il quale richiama semplicemente il metodo della implementazione Java di una coda bloccante: BlockingQueue.put

// File: SendQueue.java
public void addMessage( Message msg )
{
try {
queue.put( msg ); // questo blocca il EDT
}
// può essere ignorata poichè il thread è stato cancellato
catch( InterruptedException ignore ) { }
}

Perchè il metodo di libreria può sollevare una InterruptedException? la risposta è semplice: come scritto poc'anzi, la coda bloccante può bloccare il thread se si tenta di aggiungere un elemento ma la coda è piena: in questo caso la interruzione del thread provoca anche la interruzione del metodo put che rientra sollevando la InterruptedException.

La esecuzione in background

Poichè questo è un thread che non finisce mai, il metodo doInBackground viene eseguito in un loop infinito; a dire il vero quasi infinito poichè è necessario verificare che esso non sia stato cancellato:

// File: SendQueue.java
@Override
public Void doInBackground() throws MMException
{
while (!isCancelled()) {
... omissis ...
}
return null;
}

Il cuore del thread della coda di invio è proprio nel metodo doInBackground: il codice prima verifica se la coda è vuota oppure no ispezionando la coda bloccante col metodo peek il quale ritorna il messaggio da inviare oppure null se la coda è vuota:

// File: SendQueue.java
@Override
public Void doInBackground() throws MMException
{
while (!isCancelled()) {
Message msg = queue.peek(); // null if queue is empty
if ( msg == null ) {
Thread.sleep( SLEEP_TIME );
}
... omissis ...
}
}
Il lettore è invitato ad osservare che l'uso del metodo BlockingQueue.peek è quello che implementa la strategia di ri-trasmissione dei messaggi il cui invio non è andato a buon fine a causa di errori di comunicazione. Il metodo peek restituisce il messaggio da inviare MA NON LO RIMUOVE DALLA CODA.

Se un messaggio viene restituito dal metodo peek, esso viene passato come argomento al metodo doSendMessage che provvede alla effettiva trasmissione:

// File: SendQueue.java
@Override
public Void doInBackground() throws MMException
while (!isCancelled()) {
... omissis ...
else {
connection.doSendMessage( msg ); // questo è bloccante
queue.poll(); // rimuove il messaggio dalla coda
publish( msg ); // il messaggio è stato inviato
}
}

Poichè il metodo doSendMessage è bloccante, abbiamo solo due casi:

  • esso rientra e quindi il messaggio è stato recapitato con successo; è possibile rimuovere il messaggio dalla coda col metodo BlockingQueue.poll.
  • il metodo non rientra ma solleva una eccezione che sarà propagata al di fuori del metodo doInBackground ed intercettata dal metodo done; il thread termina ma il messaggio è ancora in coda e sarà ritrasmesso se la connessione sarà ripristinata

Elaborare i risultati intermedi

Il metodo process viene richiamato nel EDT (vedi Il Event Dispatching Thread) e ad esso vengono passati come argomento i risultati intermedi della elaborazione in background. Nel nostro caso, i risultati intermedi sono i messaggi inviati sulla connessione di rete e "pubblicati" nel metodo doInBackground:

// File: SendQueue.java
protected void process(List<Message> msgList )
{
for ( Iterator<Message> iter = msgList.iterator(); iter.hasNext(); ) {
Message item = iter.next();
logger.fine( "SendQueue.process() msgID=" + item.getID());
connection.messageSent( item.getID(), 0 );
}
}

Vi sembrerà strano che, benchè il metodo publish pubblichi uno ed un solo dato (un oggetto Message), il metodo process accetti come argomento non uno ma una lista di Message. La spiegazione è piuttosto semplice: ricordate sempre che il thread separato e il EDT vengono eseguiti in parallelo, ognuno per conto suo, e nessuno dei due è a conoscenza dello stato di avanzamento dell'altro.
Può succedere che il worker-thread pubblichi un messaggio per essere elaborato dal EDT ma quest'ultimo sia in altre faccende affaccendato e non possa quindi elaborare subito il dato pubblicato. Nel mentre, il worker-thread elabora un altro messaggio in coda e la pubblica: a questo punto vi sono DUE messaggi pubblicati e non uno solo.

Ecco perchè il metodo process accetta una lista di risultati intermedi: esso deve iterare sulla lista di risultati per poterli elaborare tutti.

SendQueue: terminazione

Come accennato in precedenza, il never-ending-thread (thread infinito) non è "infinito" in senso letterale in quanto può terminare in due modi:

  • quando la connessione viene chiusa ed il thread viene cancellato
  • in caso di eccezioni sul canale di comunicazione: è inutile tenere in esecuzione la coda di invio se ci sono errori di comunicazione

Quando il thread termina viene richiamato il metodo done dal EDT:

// File: SendQueue.java
@Override
public void done()
{
try {
get();
}
catch ( InterruptedException | CancellationException ex )
{
logger.info( "SendQueue.done() - thread finished by exception:" + ex.getClass().getName());
}
catch ( ExecutionException ex )
{
logger.warning( "SendQueue.done() - Execution exception:" + ex.getMessage());
connection.connectionError( Connection.IOperation.SEND, (MMException) ex.getCause() );
}
}

Le due eccezioni InterruptedException e CancellationException vengono ignorate: se il thread è stato interrotto o cancellato è perchè la connessione è stata chiusa probabilmente perchè la applicazione stessa sta terminando.

In caso di errori di connessione, invece, intercetteremo la ExecutionException che inoltreremo al listener degli eventi di rete.

La classe RecvQueue

Benchè il nome di questa classe evochi una coda anche per i messaggi ricevuti, la classe RecvQueue, che legge i messaggi dal canale di comunicazione, non usa davvero una coda bloccante per questa operazione. In effetti, quando un messaggio è stato ricevuto dal metodo in background e pubblicato come risultato intermedio, esso può semplicemente essere inoltrato al listener degli eventi di rete tramite il metodo di interfaccia messageReceived.

Implementare una coda in ricezione non è del tutto sbagliato; è semplicemente inutile in una applicazione come questa ma se consideriamo altri scenari, una coda in ricezione potrebbe essere una grande risorsa. Pensate ad esempio ad un server centrale che monitora decine di telecamere remote che trasmettono immagini del luogo ma solo nel momento in cui rilevano del movimento. In condizioni normali è pressochè impossibile che tutte abbiano la necessità di trasmettere dati ma ci potrebbero essere momenti particolari in cui un numero significativo di telecamere debbano trasmettere le immagini perchè si sono attivate.

La conseguenza è che il server centrale è messo sotto stress dovendo elaborare una mole di lavoro inaspettata e probabilmente non riuscirà ad elaborare tutti i dati in tempo reale con inevitabile perdita di alcune immagini. Invece di dotarci di un supercomputer che possa gestire tale mole di lavoro possiamo usare una coda bloccante in ricezione nella quale i messaggi trasmessi dalle telecamere vengono memorizzati ma non elaborati. Il server preleva un messaggio alla volta dalla coda e lo elabora nei suoi tempi. Anche in questo caso si noterà un rallentamento nelle operazioni ma è fisiologico e temporaneo: non appena la situazione si normalizzerà il flusso di dati riprenderà con i normali volumi ma l'uso della coda in ricezione ci ha permesso di non perdere nemmeno un frame, almeno fino a che la coda non si riempie.
Uno scenario come quello delle telecamere è ciò che in gergo informatico si chiama "un ambiente producer / consumer" in cui abbiamo sostanzialmente tre threads:

  • il thread principale (il EDT) che si occupa della sola GUI: esso visualizza i risultati della elaborazione dei dati e questo thread è sempre libero, non si blocca mai
  • un thread secondario che chiamiamo producer (=produttore) che riceve le immagini dalle telecamere e le accoda nella coda bloccante: questo thread può bloccarsi se la coda è piena
  • un altro thread secondario che chiameremo consumer (=consumatore) che preleva (consuma) i dati in coda e li fornisce al EDT quando questo ne fa richiesta cioè quando il EDT ha finito di elaborare una immagine e vuole passare alla successiva; il thread consumer può bloccarsi se la coda è vuota

Ciò che conta davvero in questo scenario è che il EDT non si blocca mai in una attesa di input o perchè la coda è piena: è certo che se il producer è troppo veloce si noterà un certo rallentamento nelle operazioni e le immagini non saranno visualizzate in tempo reale ma la applicazione rimane reattiva; è questo quello che conta.
Per sperimentare questo aspetto delle code bloccanti ho scritto una piccola app che usa una coda bloccante e visualizza in un componente personalizzato il riempimento della coda stessa. Questa app è descritta in Le code bloccanti.

Il metodo in background

E' molto simile a quello della coda di invio ma, ovviamente, il metodo di I/O richiamato non sarà doSendMessage bensì doReadMessage. Quando in messaggio viene ricevuto esso viene pubblicato per mezzo del metodo publish:

// File: RecvQueue.java
@Override
public Void doInBackground() throws MMException
{
try {
while (!isCancelled()) {
Message msg = connection.doReadMessage(); // questo è bloccante
if ( msg != null ) {
publish( msg ); // il messaggio è stato letto
}
else {
Thread.sleep( SLEEP_TIME );
}
}
logger.info( "RecvQueue.doInBackground() - thread cancelled" );
}
catch( InterruptedException ignore )
{
}
return null;
}

I risultati intermedi

Non c'è molto da aggiungere a quanto già visto per la coda di invio in Elaborare i risultati intermedi. Il metodo process elabora tutti i messaggi pubblicati come risultati intermedi richiamando il metodo di interfaccia del listener degli eventi di rete:

// File: RecvQueue.java
protected void process(List<Message> msgList )
{
// non accoda i messaggi ma li invia al listener
for ( Iterator<Message> iter = msgList.iterator(); iter.hasNext(); ) {
Message item = iter.next();
assert item != null : "RecvQueue.process() message is null";
logger.fine( "RecvQueue.process() msgID=" + item.getID());
connection.messageReceived( item );
}
}

La sincronizzazione

La strategia delle code bloccanti risolve egregiamente i tre problemi riscontrati in Introduzione tuttavia, non sono tutte rose e fiori. Anche questa soluzione ha il suo "lato oscuro", il "rovescio della medaglia" ma non riguarda le code bloccanti in se stesse quanto, piuttosto, l'avere a che fare con i threads separati. Mi spiego meglio con un esempio, preso proprio da mastermind. Analizziamo i due metodi principali della classe SendQueue:

// File: SendQueue.java
// il metodo addMessage
public void addMessage( Message msg )
{
... omissis ...
try {
queue.put( msg ); // inserisce il messaggio in coda
}
... omissis ...
}
// il metodo doInBackground
public Void doInBackground() throws ...
{
... omissis ...
Message msg = queue.peek(); // estrae il messaggio dalla coda
... omissis ...

Teniamo bene presente che questi due metodi vengono eseguiti in threads SEPARATI; il primo viene sempre eseguito nel EDT mentre il secondo metodo viene eseguito nel thread che chiameremo secondario (o anche worker-thread). Teniamo presente anche che ognuno dei due threads esegue per conto suo, in modo asincrono, indipendentemente l'uno dall'altro.
Per accodare un messaggio viene richiamato il metodo BlockingQueue.put il quale è implementato più o meno così:

private Message array; // una array di 50 elementi
private int counter; // il contatore di elementi effettivamente presenti
public void put( Message item )
{
while (counter >= 50 ) {
Thread.sleep(); // blocca se la coda è piena
}
// queste due istruzioni sono critiche
array[counter] = item;
counter++;
}

la coda, come tutti i containers di Kava sono in ultima analisi implementate come una array di elementi di dimensioni calcolate in modo approssimativo, per eccesso e viene mantenuto un contatore che rappresenta il numero effettivo di elementi presenti nella coda; all'inizio la coda è vuota e counter sarà pari a ZERO. Nel metodo put, l'elemento fornito come argomento viene inserito nella array all'indice espresso dal contatore. SUBITO DOPO l'inserimento dell'elemento nella array, il contatore viene aggiornato: esso rappresenta in ogni momento il numero di elementi effettivamente presenti nel contenitore e l'indice del primo elemento libero.
Notate come ho evidenziato le parole "SUBITO DOPO". Questo è il nocciolo della questione. Supponiamo che proprio quando il metodo ha aggiunto l'elemento alla array ma prima dell'aggiornamento del contatore, il thread secondario esegue la istruzione peek e preleva il primo ed unico elemento presente nella coda.
Un elemento in coda esiste ma finchè il contatore non viene aggiornato è come se non esistesse.

Quello che è accaduto è uno dei tre gravi problemi che si devono affrontare quando si ha a che fare con i threads. Il problema che ho descritto ora si chiama memory inconsistency (=inconsistenza della memoria) ed è dovuta al fatto che la maggior parte delle operazioni eseguite in una applicazione non sono atomiche, vale a dire possono essere interrotte in qualsiasi momento da un altro thread proprio nel bel mezzo di un metodo che sta aggiornando i dati interni in seguito ad una operazione.

Appare chiaro che le due istruzioni che aggiornano la array ed il contatore sono critiche: non devono assolutamente essere interrotte da un altro thread che legge la coda o, peggio, che aggiunge un altro elemento alla coda.
Quello che ci serve per risolvere il problema della inconsistenza di memoria è la sincronizzazione tra i due metodi addMessage, che inserisce un messaggio in coda, e doInBackground che preleva il messaggio dalla coda: la esecuzione dell'uno preclude la esecuzione dell'altro.
Il linguaggio Java mette a disposizione due semplici costrutti per la sincronizzazione delle operazioni:

  • i metodi sincronizzati: synchronized methods
  • le istruzioni sincronizzate: synchronized statements

I metodi sincronizzati acquiscono un blocco sull'oggetto (non sulla classe, ma sull'oggetto reale) col quale vengono richiamati. Tutti gli altri metodi sincronizzati in altri thread vengono bloccati, in attesa che il blocco venga rilasciato. Per definire un metodo sincronizzato si usa la parola chiave synchronized come attributo del metodo stesso:

public synchronized void addMessage( Message msg ) // notate l'attributo
{
... omissis ...
}

Facile,no? Siete tentati di fare lo stesso per il metodo concorrente, vero?

public synchronized Void doInBackground() // notate l'attributo
{
... omissis ..
}

Sarebbe un grave errore. Come detto poc'anzi, il metodo acquisisce il blocco automaticamente, subito prima di essere eseguito e lo rilascia solo quando il metodo termina: ma il metodo doInBackground non termina MAI (salvo cancellazione, ma a quel punto la applicazione è terminata).
Il risultato è che il metodo addMessage resta in attesa dello sblocco all'infinito e, dall'altra parte, il metodo doInBackground resterà in attesa di un messaggio da inviare all'infinito; quello che abbiamo ottenuto è il secondo grave problema di avere a che fare con i threads: il cosidetto deadlock (=il blocco della morte).

La soluzione a questo problema è quella di usare le istruzioni sincronizzate anzichè l'intero metodo; quello che ci serve sincronizzare non è l'intero metodo doInBackground ma solo una piccola parte di codice, quello tra le istruzioni peek e poll:

// File: SendQueue.java
public Void doInBackground() throws ...
{
... omissis ..
synchronized( this ) { // inizio blocco di istruzioni sincronizzate
Message msg = queue.peek();
logger.finest( "SendQueue.doInBackground() msg peek=" + msg );
if ( msg != null ) {
connection.doSendMessage( msg );
queue.poll(); // rimuove il messaggio dalla coda
publish( msg );
}
} // fine blocco di istruzioni sincronizzate

Il costrutto synchronized(this) acquisisce un blocco sull'oggetto per il quale il metodo doInBackground è stato richiamato (this) e con questi due accorgimenti abbiamo ottenuto che:

  • il metodo addMessage non potrà essere eseguito finchè il blocco di codice sincronizzato del metodo doInBackground, se in esecuzione, non sarà terminato
  • il blocco di codice sincronizzato del metodo doInBackground non potrà essere eseguito finchè il metodo addMessage, se in esecuzione, non sarà terminato

Avrete notato dai sorgenti SendQueue.java che le istruzioni di cincronizzazione dei metodi che ho esposto più sopra sono commentate; in realtà non sono affatto operative. Questo è dovuto al fatto che tutte le implementazioni Java della interfaccia BlockingQueue sono thread-safe (=sicure con i threads) e che i metodi put, peek e poll sono già automaticamente sincronizzati dalla libreria Java.

Per saperne di più sui metodi sincronizzati e su come si creano i blocchi che eseguono la sincronizzazione ho scritto una breve sezione in appendice (vedi Le operazioni atomiche).

Ulteriore documentazione

La documentazione completa dei sorgenti descritti in questo capitolo può essere visualizzata clikkando il seguente link che riporta alla documentazione Javadoc del progetto: The MasterMind Project - version 0.8

Argomento precedente - Argomento successivo - Indice Generale