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;
}
}
Point
es 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.
DelegatingVehicleTracker
en 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 MutablePoint
clase original en lugar de Point, estaríamos rompiendo la encapsulación al permitir getLocations
publicar 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 getLocations
y 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.
VisualComponent
en 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 VisualComponent
lo 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);
}
}
VisualComponent
utiliza a CopyOnWriteArrayList
para 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, VisualComponent
puede delegar sus responsabilidades de seguridad de subprocesos en el subyacentemouseListeners
keyListeners
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. NumberRange
en 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());
}
}
NumberRange
no es seguro para subprocesos ; no preserva la invariante que restringe los niveles inferiores y superiores. Los métodos setLower
y setUpper
intentan respetar esta invariante, pero lo hacen mal. Ambas setLower
y setUpper
son 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
yupper
NumberRange
no puede simplemente delegar la seguridad de subprocesos a sus variables de estado de seguridad de subprocesos.
NumberRange
podrí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 NumberRange
hace, 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.