4.3.1. Esempio: Tracker di veicoli che utilizza la delega
Come esempio più sostanziale di delega, costruiamo una versione del tracker del veicolo che delega a una classe thread-safe. Conserviamo le posizioni in una mappa, così si comincia con un'implementazione di Map thread-safe, ConcurrentHashMap
. Memorizziamo anche la posizione utilizzando una classe Point immutabile invece di MutablePoint
, mostrato nel Listato 4.6.
Listato 4.6. Classe immutabile Point utilizzata da DelegatingVehicleTracker.
class Point{
public final int x, y;
public Point() {
this.x=0; this.y=0;
}
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Point
è thread-safe perché è immutabile. I valori immutabili possono essere condivisi e pubblicati liberamente, quindi non è più necessario copiare le posizioni quando vengono restituite.
DelegatingVehicleTracker
nel Listato 4.7 non utilizza alcuna sincronizzazione esplicita; tutti gli accessi allo stato sono gestiti da ConcurrentHashMap
e tutte le chiavi e i valori della mappa sono immutabili.
Listato 4.7. Delega della thread safety a un ConcurrentHashMap.
public class DelegatingVehicleTracker {
private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String, Point> points) {
this.locations = new ConcurrentHashMap<String, Point>(points);
this.unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String, Point> getLocations(){
return this.unmodifiableMap; // User cannot update point(x,y) as Point is immutable
}
public Point getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if(locations.replace(id, new Point(x, y)) == null) {
throw new IllegalArgumentException("invalid vehicle name: " + id);
}
}
}
Se avessimo usato la MutablePoint
classe originale invece di Point, avremmo interrotto l'incapsulamento consentendo di getLocations
pubblicare un riferimento allo stato mutabile che non è thread-safe. Si noti che abbiamo leggermente modificato il comportamento della classe tracker del veicolo; mentre la versione monitor restituiva un'istantanea delle posizioni, la versione di delega restituisce una vista non modificabile ma "live" delle posizioni dei veicoli. Ciò significa che se il thread A chiama getLocations
e il thread B modifica successivamente la posizione di alcuni punti, tali modifiche si riflettono nella mappa restituita al thread A.
4.3.2. Variabili di stato indipendenti
Possiamo anche delegare la thread safety a più di una variabile di stato sottostante purché le variabili di stato sottostanti siano indipendenti, il che significa che la classe composita non impone invarianti che coinvolgono più variabili di stato.
VisualComponent
nel Listato 4.9 è un componente grafico che consente ai client di registrare ascoltatori per eventi di mouse e battitura. Mantiene un elenco di listener registrati di ogni tipo, in modo che quando si verifica un evento possono essere richiamati i listener appropriati. Ma non esiste alcuna relazione tra l'insieme degli ascoltatori del mouse e gli ascoltatori chiave; i due sono indipendenti e pertanto VisualComponent
possono delegare i propri obblighi di thread safety a due elenchi thread-safe sottostanti.
Listato 4.9. Delega della thread safety a più variabili di stato sottostanti.
public class VisualComponent {
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();
public void addKeyListener(KeyListener listener) {
keyListeners.add(listener);
}
public void addMouseListener(MouseListener listener) {
mouseListeners.add(listener);
}
public void removeKeyListener(KeyListener listener) {
keyListeners.remove(listener);
}
public void removeMouseListener(MouseListener listener) {
mouseListeners.remove(listener);
}
}
VisualComponent
usa a CopyOnWriteArrayList
per memorizzare ogni elenco di ascoltatori; questa è un'implementazione di elenchi thread-safe particolarmente adatta per la gestione di elenchi di ascoltatori (vedere la Sezione 5.2.3). Ogni List è thread-safe e poiché non ci sono vincoli che accoppiano lo stato di uno allo stato dell'altro, VisualComponent
può delegare le proprie responsabilità di thread safety agli oggetti sottostanti mouseListeners
e keyListeners
.
4.3.3. Quando la delega fallisce
La maggior parte delle classi composite non sono semplici come VisualComponent
: hanno invarianti che mettono in relazione le variabili di stato dei componenti. NumberRange
nel Listato 4.10 utilizza due AtomicIntegers
per gestire il proprio stato, ma impone un vincolo aggiuntivo: il primo numero è minore o uguale al secondo.
Listato 4.10. Classe di intervallo di numeri che non protegge sufficientemente le sue invarianti. Non farlo.
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
//Warning - unsafe check-then-act
if(i > upper.get()) {
throw new IllegalArgumentException(
"Can't set lower to " + i + " > upper ");
}
lower.set(i);
}
public void setUpper(int i) {
//Warning - unsafe check-then-act
if(i < lower.get()) {
throw new IllegalArgumentException(
"Can't set upper to " + i + " < lower ");
}
upper.set(i);
}
public boolean isInRange(int i){
return (i >= lower.get() && i <= upper.get());
}
}
NumberRange
non è thread-safe ; non preserva l'invariante che vincola inferiore e superiore. I metodi setLower
e setUpper
tentano di rispettare questa invariante, ma lo fanno male. Entrambi setLower
e setUpper
sono sequenze check-allora-atto, ma non usano bloccaggio sufficiente a renderli atomica. Se l'intervallo di numeri contiene (0, 10) e un thread chiama setLower(5)
mentre un altro thread chiama setUpper(4)
, con un tempismo sfortunato entrambi supereranno i controlli nei setter e verranno applicate entrambe le modifiche. Il risultato è che l'intervallo ora contiene (5, 4): uno stato non valido . Quindi, mentre gli AtomicInteger sottostanti sono thread-safe, la classe composita non lo è . Perché le variabili di stato sottostanti lower
eupper
non sono indipendenti,NumberRange
non può semplicemente delegare la thread safety alle sue variabili di stato thread-safe.
NumberRange
potrebbe essere reso thread-safe utilizzando il blocco per mantenere le sue invarianti, come la protezione inferiore e superiore con un blocco comune. Deve anche evitare la pubblicazione in basso e in alto per impedire ai client di sovvertire le sue invarianti.
Se una classe ha azioni composte, così come NumberRange
, la delega da sola non è di nuovo un approccio adatto per la sicurezza dei thread. In questi casi, la classe deve fornire il proprio blocco per garantire che le azioni composte siano atomiche, a meno che l'intera azione composta non possa essere delegata anche alle variabili di stato sottostanti.
Se una classe è composta da più variabili di stato thread-safe indipendenti e non ha operazioni con transizioni di stato non valide, può delegare la thread safety alle variabili di stato sottostanti.