4.3.1. Przykład: śledzenie pojazdu przy użyciu delegacji
Jako bardziej istotny przykład delegowania, skonstruujmy wersję modułu śledzącego pojazdy, która deleguje do klasy bezpiecznej wątkowo. Możemy zapisać lokalizacje w mapę, więc zacząć od wdrożenia Mapa thread-safe, ConcurrentHashMap
. Lokalizację przechowujemy również przy użyciu niezmiennej klasy Point zamiast MutablePoint
, jak pokazano na listingu 4.6.
Listing 4.6. Immutable Point Klasa używana przez 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
jest bezpieczny dla wątków, ponieważ jest niezmienny. Niezmienne wartości można swobodnie udostępniać i publikować, dzięki czemu nie musimy już kopiować lokalizacji podczas ich zwracania.
DelegatingVehicleTracker
na listingu 4.7 nie używa żadnej jawnej synchronizacji; cały dostęp do stanu jest zarządzany przez ConcurrentHashMap
, a wszystkie klucze i wartości mapy są niezmienne.
Listing 4.7. Delegowanie bezpieczeństwa wątków do 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);
}
}
}
Gdybyśmy użyli oryginalnej MutablePoint
klasy zamiast Point, zerwalibyśmy hermetyzację, zezwalając na getLocations
publikację odwołania do mutowalnego stanu, który nie jest bezpieczny dla wątków. Zwróć uwagę, że nieznacznie zmieniliśmy zachowanie klasy śledzenia pojazdów; podczas gdy wersja monitorująca zwróciła migawkę lokalizacji, wersja delegująca zwraca niemodyfikowalny, ale „na żywo” widok lokalizacji pojazdów. Oznacza to, że jeśli wątek A wywoła, getLocations
a wątek B później zmodyfikuje lokalizację niektórych punktów, te zmiany zostaną odzwierciedlone w mapie zwróconej do wątku A.
4.3.2. Niezależne zmienne stanu
Możemy również delegować bezpieczeństwo wątków do więcej niż jednej podstawowej zmiennej stanu, o ile te podstawowe zmienne stanu są niezależne, co oznacza, że klasa złożona nie narzuca żadnych niezmienników obejmujących wiele zmiennych stanu.
VisualComponent
na listingu 4.9 jest komponentem graficznym, który umożliwia klientom rejestrowanie nasłuchiwania zdarzeń myszy i naciśnięć klawiszy. Utrzymuje listę zarejestrowanych detektorów każdego typu, dzięki czemu w przypadku wystąpienia zdarzenia można wywołać odpowiednie nasłuchiwanie. Nie ma jednak związku między zbiorem słuchaczy myszy i słuchaczy kluczy; te dwa są niezależne i dlatego VisualComponent
mogą delegować swoje zobowiązania dotyczące bezpieczeństwa wątków do dwóch bazowych list bezpiecznych wątków.
Listing 4.9. Delegowanie bezpieczeństwa wątków do wielu bazowych zmiennych stanu.
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
używa a CopyOnWriteArrayList
do przechowywania każdej listy słuchaczy; jest to bezpieczna dla wątków implementacja list, szczególnie odpowiednia do zarządzania listami słuchaczy (zobacz Rozdział 5.2.3). Każda lista jest bezpieczna dla wątków, a ponieważ nie ma żadnych ograniczeń łączących stan jednego ze stanem drugiego, VisualComponent
może delegować obowiązki związane z bezpieczeństwem wątków na elementy bazowe mouseListeners
i keyListeners
obiekty.
4.3.3. Gdy delegacja się nie powiedzie
Większość klas złożonych nie jest tak prosta, jak VisualComponent
: mają niezmienniki, które wiążą swoje składowe zmienne stanu. NumberRange
na listingu 4.10 używa dwóch AtomicIntegers
do zarządzania swoim stanem, ale nakłada dodatkowe ograniczenie - pierwsza liczba jest mniejsza lub równa drugiej.
Listing 4.10. Klasa zakresu liczb, która nie chroni wystarczająco swoich niezmienników. Nie rób tego.
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
nie jest bezpieczny dla wątków ; nie zachowuje niezmiennika, który ogranicza dolną i górną część. setLower
I setUpper
metody próbować respektować tę niezmiennik, ale czy tak źle. Obie setLower
i setUpper
są sekwencjami check-then-act, ale nie używają wystarczającego blokowania, aby uczynić je atomowymi. Jeśli zakres numerów obejmuje (0, 10), a jeden wątek wywołuje, setLower(5)
podczas gdy inny wątek wywołuje setUpper(4)
, z pewnym niefortunnym czasem, oba przejdą sprawdzanie w ustawieniach i obie modyfikacje zostaną zastosowane. W rezultacie zakres zawiera teraz (5, 4) - stan nieprawidłowy . Tak więc, podczas gdy podstawowe AtomicIntegers są bezpieczne dla wątków, klasa złożona nie jest . Ponieważ bazowe zmienne stanu lower
iupper
nie są niezależne, NumberRange
nie mogą po prostu delegować bezpieczeństwa wątków do swoich zmiennych stanu bezpiecznych dla wątków.
NumberRange
można by uczynić bezpiecznymi dla wątków poprzez zastosowanie blokowania w celu zachowania jego niezmienności, takich jak osłona dolnej i górnej części za pomocą wspólnego zamka. Musi również unikać publikowania dolnej i górnej części, aby klienci nie mogli podważać swoich niezmienników.
Jeśli klasa ma akcje złożone, NumberRange
to samo delegowanie ponownie nie jest odpowiednim podejściem do bezpieczeństwa wątków. W takich przypadkach klasa musi zapewnić własne blokowanie, aby zapewnić, że akcje złożone są niepodzielne, chyba że cała akcja złożona może być również delegowana do bazowych zmiennych stanu.
Jeśli klasa składa się z wielu niezależnych zmiennych stanu bezpiecznych dla wątków i nie ma operacji, które mają jakiekolwiek nieprawidłowe przejścia stanu, może delegować bezpieczeństwo wątków do bazowych zmiennych stanu.