Di norma, i thread procedono in modo indipendente.
Ogni oggetto di qualsiasi classe ha un cosidetto monitor associato. È come un corridoio stretto quanto basta per permettere il passaggio di un solo thread per volta.
Un blocco synchronized("abcd") { … } devia il thread verso quella strettoia. Nella strettoia passa un solo thread per volta. Il primo che arriva entra, il secondo e i successivi devono aspettare che il primo sia uscito dall'altra parte.
| | |
| | |
| | |
\ / |
\ + |
\ |
||| |||
||| String ||| String
||| "abcd" ||| "efgh"
||| |||
/ |
/ |
/ |
| |
| |
V V
Il primo thread arriva per primo alla strettoia dell'oggetto stringa "abcd" ed entra. Il secondo trova la strettoia occupata, per cui si deve fermare e aspettare che il primo sia uscito.
Nel frattempo, il terzo thread va verso la strettoia di "efgh". Dato che è una stringa diversa ha una sua strettoia, che non è al momento occupata. Il terzo thread non viene bloccato, al contrario del secondo, e può passare.
Per evitare che due o più thread accedano in contemporanea agli stessi dati, si fanno passare tutti per la stessa strettoia. Le istruzioni di un thread che si trovano nel blocco synchronized(…) { … } non vengono mai eseguite contemporaneamente. Il primo thread che arriva a quel punto le esegue, i successivi vengono bloccati e aspettano che il primo abbia terminato.
Il problema è garantire che la strettoia sia la stessa per tutti i thread. Dato che ogni oggetto ha una sua strettoia, serve uno stesso oggetto per tutti.
Per garantire che uno solo di tutti i thread possibili abbia accesso alle classi dati in qualsiasi momento, si fanno passare tutti attraverso lo stesso corridoio dello stesso oggetto. Questo può essere l'oggetto unico della classe Unico vista prima.
| | /
| | /
| | /
\ / /
\ + /
\ +
||| Unico
|||
/
/
/
|
|
V
[tornelli-03.fig]
// attivita' atomica
private void verificaperiodica(HashSet<Autobus> autobus) {
…
}
// attivita' atomica
private void verificarotture(HashSet<Autobus> autobus) {
…
}
|
|
// attivita' atomica
private void verificaperiodica(HashSet<Autobus> autobus) {
synchronized(Unico.creaUnico()) {
…
}
}
// attivita' atomica
private void verificarotture(HashSet<Autobus> autobus) {
synchronized(Unico.creaUnico()) {
…
}
}
|
Il blocco synchronized(unico) { … } garantisce che i thread delle attività atomiche non vengano eseguiti contemporaneamente. Va però inserito in ognuna delle attività atomiche quando vengono scritte. Non è detto che il programmatore si ricordi di farlo.
La soluzione è sincronizzare attraverso una classe comune, che viene scritta una volta sola. Un metodo sincronizzato della classe Unico passa per l'unica strettoia dell'oggetto unico.
public class Unico {
…
public synchronized void perform() {
…
}
}
Un thread che esegue il metodo perform() viene deviato verso la strettoia dell'oggetto singolo. Un secondo metodo che arriva allo stesso punto viene bloccato fino a che il primo non ha finito.
Questo è quello che deve succedere con le istruzioni di tutte le attività atomiche. Queste istruzioni vanno quindi eseguite nel metodo perform(). Essendo istruzioni diverse per diverse attività atomiche, non si possono scrivere direttamente in perform().
Si possono però passare a perform() inserendole in oggetti funtore.
public class VerificaPeriodica {
public void esegui() {
…
}
}
public class VerificaRotture {
public void esegui() {
…
}
}
public class Unico {
…
public synchronized void esegui(Attivita a) {
a.esegui();
}
}
Questo richiede che le attività implementino l'interfaccia Attivita con un metodo esegui(). La versione definitiva delle attività atomiche:
public interface Attivita {
public void esegui();
}
public class VerificaPeriodica implements Attivita {
public void esegui() {
}
}
public class VerificaRotture implements Attivita {
public void esegui() {
}
}
Le attività atomiche sono funtori che implementano l'iterfaccia Attivita e hanno quindi un metodo esegui senza argomenti e senza risultato.
Non vengono eseguite direttamente ma attraverso il metodo perform() dell'oggetto unico, che passa per la strettoia dell'oggetto unico.
vp = new VerificaPeriodica(…); Unico.creaUnico().esegui(vp); … vr = new VerificaRotture(…); Unico.creaUnico().esegui(vr);
Il metodo esegui() dell'attività viene eseguito nel corso dell'esecuzione di perform() dell'oggetto unico, che avviene nella strettoia dell'oggetto unico.
L'implementazione sincronizza gli accessi alle classi dati solo se le attività atomiche vengono eseguite con Unico.creaUnico().esegui(attivita);.
È però ancora possibile sbagliare eseguendole direttamente, con attivita.esegui();.
Si può vietare l'esecuzione diretta con un oggetto gettore creato nella classe Unico. Si permette la creazione dell'oggetto gettone nella classe Unico e lo si crea in perform(), che lo passa all'attività, che verifica che non sia nullo prima di eseguirsi. Un esempio di implementazione: Unico.java, Attivita.java, VerificaPeriodica.java Main.java. Non non viene fatto per semplicità: