4.3.1. Beispiel: Vehicle Tracker mit Delegation
Als umfangreicheres Beispiel für die Delegierung erstellen wir eine Version des Fahrzeug-Trackers, die an eine thread-sichere Klasse delegiert. Wir speichern die Standorte in einer Karte und beginnen daher mit einer thread-sicheren Kartenimplementierung ConcurrentHashMap
. Wir speichern den Speicherort auch mit einer unveränderlichen Punktklasse anstelle von MutablePoint
Listing 4.6.
Listing 4.6. Unveränderliche Punktklasse, die von DelegatingVehicleTracker verwendet wird.
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
ist threadsicher, weil es unveränderlich ist. Unveränderliche Werte können frei geteilt und veröffentlicht werden, sodass wir die Speicherorte bei der Rückgabe nicht mehr kopieren müssen.
DelegatingVehicleTracker
in Listing 4.7 wird keine explizite Synchronisation verwendet; Der gesamte Zugriff auf den Status wird von verwaltet ConcurrentHashMap
, und alle Schlüssel und Werte der Karte sind unveränderlich.
Listing 4.7. Delegieren der Thread-Sicherheit an eine 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);
}
}
}}
Wenn wir die ursprüngliche MutablePoint
Klasse anstelle von Point verwendet hätten, würden wir die Kapselung unterbrechen getLocations
, indem wir einen Verweis auf einen veränderlichen Zustand veröffentlichen lassen, der nicht threadsicher ist. Beachten Sie, dass wir das Verhalten der Fahrzeug-Tracker-Klasse geringfügig geändert haben. Während die Monitorversion eine Momentaufnahme der Standorte zurückgibt, gibt die delegierende Version eine nicht veränderbare, aber "Live" -Ansicht der Fahrzeugstandorte zurück. Dies bedeutet, dass wenn Thread A aufruft getLocations
und Thread B später die Position einiger Punkte ändert, diese Änderungen in der Map widergespiegelt werden, die an Thread A zurückgegeben wird.
4.3.2. Unabhängige Zustandsvariablen
Wir können die Thread-Sicherheit auch an mehr als eine zugrunde liegende Zustandsvariable delegieren, solange diese zugrunde liegenden Zustandsvariablen unabhängig sind. Dies bedeutet, dass die zusammengesetzte Klasse keine Invarianten auferlegt, an denen mehrere Zustandsvariablen beteiligt sind.
VisualComponent
in Listing 4.9 ist eine grafische Komponente, mit der Clients Listener für Maus- und Tastenanschlagereignisse registrieren können. Es wird eine Liste der registrierten Listener jedes Typs geführt, sodass bei Auftreten eines Ereignisses die entsprechenden Listener aufgerufen werden können. Es gibt jedoch keine Beziehung zwischen den Maushörern und den Tastenhörern. Die beiden sind unabhängig und können daher VisualComponent
ihre Thread-Sicherheitsverpflichtungen an zwei zugrunde liegende Thread-sichere Listen delegieren.
Listing 4.9. Delegieren der Thread-Sicherheit an mehrere zugrunde liegende Statusvariablen.
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
verwendet a CopyOnWriteArrayList
, um jede Listenerliste zu speichern; Dies ist eine thread-sichere Listenimplementierung, die sich besonders zum Verwalten von Listener-Listen eignet (siehe Abschnitt 5.2.3). Jede Liste ist threadsicher. Da es keine Einschränkungen gibt, die den Status der einen mit dem Status der anderen VisualComponent
koppeln , kann sie ihre Thread-Sicherheitsverantwortung an den Basiswert mouseListeners
und die keyListeners
Objekte delegieren .
4.3.3. Wenn die Delegierung fehlschlägt
Die meisten zusammengesetzten Klassen sind nicht so einfach wie VisualComponent
: Sie haben Invarianten, die ihre Komponentenzustandsvariablen in Beziehung setzen. NumberRange
In Listing 4.10 werden zwei verwendet AtomicIntegers
, um den Status zu verwalten. Es wird jedoch eine zusätzliche Einschränkung auferlegt: Die erste Zahl muss kleiner oder gleich der zweiten sein.
Listing 4.10. Zahlenbereichsklasse, die ihre Invarianten nicht ausreichend schützt. Tu das nicht.
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
ist nicht threadsicher ; Die Invariante, die die unteren und oberen Grenzen einschränkt, bleibt nicht erhalten. Die Methoden setLower
und setUpper
versuchen, diese Invariante zu respektieren, tun dies jedoch schlecht. Beide setLower
und setUpper
sind Check-Then-Act-Sequenzen, verwenden jedoch keine ausreichende Verriegelung, um sie atomar zu machen. Wenn der Nummernkreis gilt (0, 10) und ein Thread anruft, setLower(5)
während ein anderer Thread anruft setUpper(4)
, bestehen beide mit etwas unglücklichem Timing die Prüfungen in den Setzern und beide Modifikationen werden angewendet. Das Ergebnis ist, dass der Bereich jetzt (5, 4) enthält - ein ungültiger Zustand . So , während die zugrunde liegenden AtomicIntegers Thread-sicher sind, ist die zusammengesetzte Klasse nicht . Weil die zugrunde liegenden Zustandsvariablen lower
undupper
sind nicht unabhängig, NumberRange
können die Thread-Sicherheit nicht einfach an ihre thread-sicheren Statusvariablen delegieren.
NumberRange
könnte fadensicher gemacht werden, indem Verriegelungen verwendet werden, um die Invarianten aufrechtzuerhalten, z. B. das Schützen der unteren und oberen mit einer gemeinsamen Verriegelung. Es muss auch vermieden werden, untere und obere zu veröffentlichen, um zu verhindern, dass Clients ihre Invarianten untergraben.
Wenn eine Klasse zusammengesetzte Aktionen NumberRange
hat, ist die Delegierung allein wiederum kein geeigneter Ansatz für die Thread-Sicherheit. In diesen Fällen muss die Klasse eine eigene Sperre bereitstellen, um sicherzustellen, dass zusammengesetzte Aktionen atomar sind, es sei denn, die gesamte zusammengesetzte Aktion kann auch an die zugrunde liegenden Statusvariablen delegiert werden.
Wenn eine Klasse aus mehreren unabhängigen threadsicheren Statusvariablen besteht und keine Operationen mit ungültigen Statusübergängen aufweist, kann sie die Thread-Sicherheit an die zugrunde liegenden Statusvariablen delegieren.