4.3.1. Exemple: Suivi de véhicule utilisant la délégation
Comme exemple plus substantiel de délégation, construisons une version du suiveur de véhicule qui délègue à une classe thread-safe. Nous stockons les emplacements sur une carte, donc nous commençons par un fil de sécurité mise en œuvre de la carte, ConcurrentHashMap
. Nous stockons également l'emplacement en utilisant une classe Point immuable au lieu de MutablePoint
, comme indiqué dans l'extrait 4.6.
Listing 4.6. Classe de point immuable utilisée par 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
est thread-safe car il est immuable. Les valeurs immuables peuvent être librement partagées et publiées, nous n'avons donc plus besoin de copier les emplacements lors de leur renvoi.
DelegatingVehicleTracker
dans le Listing 4.7 n'utilise aucune synchronisation explicite; tous les accès à l'état sont gérés par ConcurrentHashMap
, et toutes les clés et valeurs de la carte sont immuables.
Listing 4.7. Délégation de la sécurité des threads à 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);
}
}
}
Si nous avions utilisé la MutablePoint
classe d' origine au lieu de Point, nous casserions l'encapsulation en laissant getLocations
publier une référence à l'état mutable qui n'est pas thread-safe. Notez que nous avons légèrement modifié le comportement de la classe de suivi de véhicule; tandis que la version moniteur a renvoyé un instantané des emplacements, la version déléguée renvoie une vue non modifiable mais «en direct» des emplacements des véhicules. Cela signifie que si le thread A appelle getLocations
et que le thread B modifie ultérieurement l'emplacement de certains des points, ces modifications sont reflétées dans la mappe renvoyée au thread A.
4.3.2. Variables d'état indépendant
Nous pouvons également déléguer la sécurité des threads à plus d'une variable d'état sous-jacente tant que ces variables d'état sous-jacentes sont indépendantes, ce qui signifie que la classe composite n'impose aucun invariant impliquant les multiples variables d'état.
VisualComponent
dans le Listing 4.9 est un composant graphique qui permet aux clients d'enregistrer des écouteurs pour les événements de souris et de frappe. Il maintient une liste d'écouteurs enregistrés de chaque type, de sorte que lorsqu'un événement se produit, les écouteurs appropriés peuvent être appelés. Mais il n'y a pas de relation entre l'ensemble des écouteurs de souris et des écouteurs clés; les deux sont indépendants et VisualComponent
peuvent donc déléguer ses obligations de sécurité de thread à deux listes thread-safe sous-jacentes.
Liste 4.9. Délégation de la sécurité des threads à plusieurs variables d'état sous-jacentes.
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
utilise a CopyOnWriteArrayList
pour stocker chaque liste d'écouteurs; il s'agit d'une implémentation de List sécurisée pour les threads particulièrement adaptée à la gestion des listes d'écouteurs (voir Section 5.2.3). Chaque liste est thread-safe, et comme il n'y a pas de contraintes couplant l'état de l'un à l'état de l'autre, VisualComponent
peut déléguer ses responsabilités de sécurité des threads aux objets mouseListeners
et aux keyListeners
objets sous - jacents .
4.3.3. Lorsque la délégation échoue
La plupart des classes composites ne sont pas aussi simples que VisualComponent
: elles ont des invariants qui relient leurs variables d'état de composant. NumberRange
dans le Listing 4.10 utilise deux AtomicIntegers
pour gérer son état, mais impose une contrainte supplémentaire - que le premier nombre soit inférieur ou égal au second.
Listing 4.10. Classe de plage de nombres qui ne protège pas suffisamment ses invariants. Ne fais pas ça.
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
n'est pas thread-safe ; il ne préserve pas l'invariant qui contraint inférieur et supérieur. Les méthodes setLower
et setUpper
tentent de respecter cet invariant, mais le font mal. Les deux setLower
et setUpper
sont des séquences de contrôle puis d'action, mais ils n'utilisent pas un verrouillage suffisant pour les rendre atomiques. Si la plage de nombres tient (0, 10), et qu'un thread appelle setLower(5)
tandis qu'un autre thread appelle setUpper(4)
, avec un certain timing malchanceux, les deux passeront les vérifications dans les setters et les deux modifications seront appliquées. Le résultat est que la plage contient maintenant (5, 4) - un état invalide . Ainsi, alors que les AtomicIntegers sous-jacents sont thread-safe, la classe composite ne l'est pas . Parce que les variables d'état sous-jacenteslower
etupper
ne sont pas indépendants, NumberRange
ne peuvent pas simplement déléguer la sécurité des threads à ses variables d'état thread-safe.
NumberRange
pourrait être rendu sans fil en utilisant le verrouillage pour maintenir ses invariants, tels que la protection inférieure et supérieure avec un verrou commun. Il doit également éviter de publier des valeurs inférieures et supérieures pour empêcher les clients de subvertir ses invariants.
Si une classe a des actions composées, comme le NumberRange
fait, la délégation seule n'est pas encore une approche appropriée pour la sécurité des threads. Dans ces cas, la classe doit fournir son propre verrouillage pour garantir que les actions composées sont atomiques, sauf si l'action composée entière peut également être déléguée aux variables d'état sous-jacentes.
Si une classe est composée de plusieurs variables d'état indépendantes pour les threads et n'a pas d'opérations qui ont des transitions d'état non valides, alors elle peut déléguer la sécurité des threads aux variables d'état sous-jacentes.