Gli oggetti con un comportamento hanno ognuno un thread.
Per esempio, se esistono tre oggetti Autobus a1, a1, a1, due oggetti Autorimessa r1, r2 e quattro oggetti Autista u1, u2, u3, u4, allora servono tre, due e quattro thread, quindi nove in totale.
Ogni thread riceve i messaggi di un oggetto e si comporta di conseguenza, eventualmente inviando altri messaggi. Per esempio, un oggetto a2 può ricevere un messaggio da un autista u3, e mandare di conseguenza un messaggio all'autorimessa r2.
Questo richiede che un thread mandi un messaggio a un altro thread. Dato che i thread procedono tutti insieme, più messaggi potrebbero venire inviati insieme. Il ricevente li riceverà uno per volta.
Quando il thread di a2 riceve un messaggio da u3, deve fare delle operazioni. Dal momento che in Java le operazioni si possono inserire solo in un metodo, deve essere eseguito quel metodo.
Questo metodo non può venire eseguito direttamente dal thread di u3. Anche se si tratta di un metodo della classe Autobus, verrebbe comunque eseguito dal thread di u3.
Viene lanciato un thread per ogni oggetto Autobus, ma i thread per Java non sono collegati agli oggetti Autobus. Il thread di a2 per Java è un thread come tutti gli altri. Come anche il thread di u3. La differenza è che nei loro metodi run() il primo esegue le azioni di a2, il secondo esegue le azioni di u3. È il primo che deve eseguire l'azione di risposta al messaggio, non il secondo. È il thread di a2, non quello di u3.
Il sistema per inviare è e ricevere messaggi sfrutta la condivisione della memoria:
Per semplicità, i messaggi potrebbero essere scritti e letti in una variabile di tipo stringa, per esempio una variabile statica di una classe. In una prima implementazione Busy.java, il destinatario controlla continuamente se questa variabile contiene una stringa invece di null.
public class Busy {
static String messaggio;
public static void main(String[] args) throws InterruptedException {
…
Busy.messaggio = null;
…
}
}
Il thread mittente inserisce il messaggio nella variabile.
class Mittente implements Runnable {
@Override
public void run() {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
Busy.messaggio = "ciao";
System.out.print("messaggio inviato");
}
}
Il thread destinatario controlla continuamente se ci sono messaggi. Se ne trova uno, reagisce di conseguenza e lo cancella dalla variabile statica, così ne può riceverne altri.
class Destinatario implements Runnable {
@Override
public void run() {
while (Busy.messaggio == null) {
}
System.out.print(" messaggio ricevuto: ");
System.out.println("\"" + Busy.messaggio + "\"");
Busy.messaggio = null;
}
}
La ricezione di messaggio è un evento asincrono: può avvenire in qualsiasi momento. Questo momento non dipende dal thread ricevente, che non lo può prevedere. Non dipende da esso ma dagli altri thread. Ci sono tre modi di gestire gli eventi asincroni:
Il busy wait viene generalmente evitato. La prima soluzione è che il thread destinatorio sospenda la sua esecuzione finchè non ci sono messaggi. In Java, questo si può realizzare con la sincronizzazione e i metodi wait(); e notify().
Per bloccare e sbloccare un thread:
primo thread secondo thread arriva al blocco sincronizzato lo trova libero: entra arriva al blocco sincronizzato lo trova occupato: si ferma esegue delle operazioni wait() 1. sospende l'esecuzione 2. libera temporaneamente il blocco ora il blocco sincronizzato è libero entra nel blocco sincronizzato esegue delle operazioni notify() non riparte perchè il blocco è occupato esegue altre operazioni esce dal blocco sincronizzato riparte esegue altre operazioni esce dal blocco sincronizzato
Una implementazione è in Blocco.java. Contiene solo il meccanismo di blocco e sblocco, manca la scrittura e la lettura del messaggio.
Questa viene aggiunta in Messaggio.java. Il messaggio viene scritto in una variabile statica stringa Messaggio.messaggio. Il blocco e lo sblocco avvengono sulla stringa letterale "abcd".
Dato che i thread procedono insieme, uno stesso thread può ricevere più messaggi insieme.
I messaggi vanno letti uno per volta. Un modo è scriverli in fondo a una lista e leggerli dalla cima, o viceversa.
Una prima implementazione Coda.java usa una lista memorizzata in una variabile statica su cui scrive un messaggio in un thread e ne legge uno in un altro.
Invece di usare "abcd" per la sincronizzazione, il blocco e lo sblocco, usa la lista stessa. Non cambia niente, dato che potrebbe essere un oggetto qualsiasi al posto di "abcd", basta che sia lo stesso sia per la sincronizzazione che per wait() e notify(). Non si può fare nell'implementazione precedente con singola variabile String perchè l'oggetto memorizzato nella variabile statica è inizialmente null e cambia ogni volta che la variabile viene cambiata. Invece l'oggetto lista rimane lo stesso, cambia solo il suo contenuto.
Una seconda implementazione Iterato.java legge e scrive più messaggi nello stesso modo.
La comunicazione da più thread verso uno passa per una lista collegata.
Lo stesso sistema si può usare nel caso più generale in cui sia i generatori di messaggi che i loro ricevitori sono più di uno.
La differenza è che ora più thread possono essere bloccati in attesa che qualcuno metta un messaggio in lista quando questa è vuota.
Sono possibili tre casi:
L'implementazione UML usa una lista per ognuno dei destinatari. Ricade quandi nella situazione specifica di singolo destinatario.
Per ogni oggetto con un comportamento viene creata una lista e lanciato un thread. Questo avviene per ogni oggetto Autobus, per ogni oggetto Autorimessa, e per ogni oggetto Autista. Per ognuno di loro c'è una lista e un thread:
while (true) {
evento = listamessaggi.get(0); // bloccante se lista vuota
destinatario.fired(evento);
}
i messaggi vengono messi nella lista da un metodo statico
Environment.aggiungiEvento(evento)