4.3.1. Ejemplo: Rastreador de vehículos con delegación
Como un ejemplo más sustancial de delegación, construyamos una versión del rastreador de vehículos que delegue a una clase segura para subprocesos. Almacenamos las ubicaciones en un mapa, así que empezamos con una implementación Mapa flujos seguros, ConcurrentHashMap. También almacenamos la ubicación utilizando una clase de Punto inmutable en lugar de MutablePoint, como se muestra en el Listado 4.6.
Listado 4.6. Clase de punto inmutable utilizada por 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;
}
}
Pointes seguro para subprocesos porque es inmutable. Los valores inmutables se pueden compartir y publicar libremente, por lo que ya no necesitamos copiar las ubicaciones al devolverlos.
DelegatingVehicleTrackeren el Listado 4.7 no usa ninguna sincronización explícita; todo el acceso al estado es administrado por ConcurrentHashMap, y todas las claves y valores del Mapa son inmutables.
Listado 4.7. Delegación de seguridad de subprocesos 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);
}
}
}
Si hubiéramos usado la MutablePointclase original en lugar de Point, estaríamos rompiendo la encapsulación al permitir getLocationspublicar una referencia al estado mutable que no sea seguro para subprocesos. Tenga en cuenta que hemos cambiado ligeramente el comportamiento de la clase de rastreador de vehículos; mientras que la versión del monitor devolvió una instantánea de las ubicaciones, la versión de delegación devuelve una vista no modificable pero "en vivo" de las ubicaciones del vehículo. Esto significa que si el hilo A llama getLocationsy el hilo B luego modifica la ubicación de algunos de los puntos, esos cambios se reflejan en el Mapa devuelto al hilo A.
4.3.2. Variables estatales independientes
También podemos delegar la seguridad de subprocesos a más de una variable de estado subyacente siempre que esas variables de estado subyacentes sean independientes, lo que significa que la clase compuesta no impone invariantes que involucren las variables de estado múltiple.
VisualComponenten el Listado 4.9 es un componente gráfico que permite a los clientes registrar oyentes para eventos de mouse y pulsaciones de teclas. Mantiene una lista de oyentes registrados de cada tipo, de modo que cuando se produce un evento se puede invocar a los oyentes apropiados. Pero no existe una relación entre el conjunto de oyentes del mouse y los oyentes clave; los dos son independientes y, por VisualComponentlo tanto, pueden delegar sus obligaciones de seguridad de subprocesos en dos listas subyacentes seguras para subprocesos.
Listado 4.9. Delegación de seguridad de subprocesos a múltiples variables de estado subyacentes.
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);
}
}
VisualComponentutiliza a CopyOnWriteArrayListpara almacenar cada lista de oyentes; Esta es una implementación de Lista segura para subprocesos especialmente adecuada para administrar listas de oyentes (ver Sección 5.2.3). Cada lista es segura para subprocesos y, dado que no existen restricciones que acoplen el estado de uno con el estado de la otra, VisualComponentpuede delegar sus responsabilidades de seguridad de subprocesos en el subyacentemouseListenerskeyListeners objetos y .
4.3.3. Cuando falla la delegación
La mayoría de las clases compuestas no son tan simples como VisualComponent: tienen invariantes que relacionan las variables de estado de sus componentes. NumberRangeen el Listado 4.10 usa dosAtomicIntegers para administrar su estado, pero impone una restricción adicional: que el primer número sea menor o igual que el segundo.
Listado 4.10. Clase de rango de números que no protege suficientemente sus invariantes. No hagas esto.
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());
}
}
NumberRangeno es seguro para subprocesos ; no preserva la invariante que restringe los niveles inferiores y superiores. Los métodos setLowery setUpperintentan respetar esta invariante, pero lo hacen mal. Ambas setLowery setUpperson secuencias de comprobar y luego actuar, pero no utilizan el bloqueo suficiente para hacerlas atómicas. Si el rango de números se mantiene (0, 10), y un hilo llama setLower(5)mientras otro llama setUpper(4), con un tiempo desafortunado ambos pasarán las verificaciones en los setters y se aplicarán ambas modificaciones. El resultado es que el rango ahora contiene (5, 4), un estado no válido . Entonces, mientras que los AtomicIntegers subyacentes son seguros para subprocesos, la clase compuesta no lo es . Debido a que las variables de estado subyacentes no son independientes,lower yupperNumberRange no puede simplemente delegar la seguridad de subprocesos a sus variables de estado de seguridad de subprocesos.
NumberRangepodría hacerse a prueba de hilos mediante el uso de bloqueo para mantener sus invariantes, como la protección inferior y superior con un bloqueo común. También debe evitar publicar más y menos para evitar que los clientes subviertan sus invariantes.
Si una clase tiene acciones compuestas, como lo NumberRangehace, la delegación sola nuevamente no es un enfoque adecuado para la seguridad de subprocesos. En estos casos, la clase debe proporcionar su propio bloqueo para garantizar que las acciones compuestas sean atómicas, a menos que toda la acción compuesta también se pueda delegar a las variables de estado subyacentes.
Si una clase se compone de múltiples variables de estado independientes seguras para subprocesos y no tiene operaciones que tengan transiciones de estado no válidas, puede delegar la seguridad de subprocesos a las variables de estado subyacentes.