4.3.1 Exemplo: Rastreador de veículos usando delegação
Como um exemplo mais substancial de delegação, vamos construir uma versão do rastreador de veículo que delega para uma classe de thread-safe. Armazenamos os locais em um mapa e, portanto, começamos com uma implementação de mapa com segurança para threads ConcurrentHashMap
. Também armazenamos o local usando uma classe Point imutável em vez de MutablePoint
, mostrada na Listagem 4.6.
Listagem 4.6. Classe de ponto imutável usada pelo 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
é seguro para threads porque é imutável. Os valores imutáveis podem ser compartilhados e publicados livremente, portanto, não precisamos mais copiar os locais ao devolvê-los.
DelegatingVehicleTracker
na Listagem 4.7 não usa nenhuma sincronização explícita; todo o acesso ao estado é gerenciado ConcurrentHashMap
e todas as chaves e valores do mapa são imutáveis.
Listagem 4.7. Delegando a segurança do encadeamento a um 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);
}
}
}
Se tivéssemos usado a MutablePoint
classe original em vez de Point, estaríamos quebrando o encapsulamento, deixando getLocations
publicar uma referência ao estado mutável que não é seguro para threads. Observe que alteramos um pouco o comportamento da classe rastreador de veículos; enquanto a versão do monitor retornou uma captura instantânea dos locais, a versão delegada retorna uma visualização inalterável, mas "ao vivo", dos locais dos veículos. Isso significa que se o segmento A chama getLocations
e o segmento B modifica posteriormente o local de alguns dos pontos, essas alterações são refletidas no mapa retornado ao segmento A.
4.3.2 Variáveis de estado independentes
Também podemos delegar a segurança do encadeamento para mais de uma variável de estado subjacente, desde que as variáveis de estado subjacentes sejam independentes, o que significa que a classe composta não impõe nenhum invariável envolvendo as variáveis de estado múltiplas.
VisualComponent
na Listagem 4.9 é um componente gráfico que permite aos clientes registrar ouvintes para eventos de mouse e pressionamento de tecla. Ele mantém uma lista de ouvintes registrados de cada tipo, para que, quando um evento ocorrer, os ouvintes apropriados possam ser chamados. Mas não há relação entre o conjunto de ouvintes de mouse e ouvintes principais; os dois são independentes e, portanto, VisualComponent
podem delegar suas obrigações de segurança de encadeamento em duas listas subjacentes de segurança de encadeamento.
Listagem 4.9. Delegando a segurança do encadeamento a várias variáveis de estado subjacente.
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
usa a CopyOnWriteArrayList
para armazenar cada lista de ouvintes; Esta é uma implementação de lista segura para threads, particularmente adequada para gerenciar listas de ouvintes (consulte a Seção 5.2.3). Cada lista é segura para threads e, como não há restrições que acoplem o estado de um ao estado do outro, VisualComponent
pode delegar suas responsabilidades de segurança de thread no subjacente mouseListeners
e nos keyListeners
objetos.
4.3.3 Quando a delegação falha
A maioria das classes compostas não é tão simples quanto VisualComponent
: elas possuem invariantes que relacionam suas variáveis de estado componentes. NumberRange
na Listagem 4.10 usa dois AtomicIntegers
para gerenciar seu estado, mas impõe uma restrição adicional - que o primeiro número seja menor ou igual ao segundo.
Listagem 4.10. Classe de intervalo de números que não protege suficientemente seus invariantes. Não faça isso.
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ão é seguro para threads ; não preserva o invariante que restringe o inferior e o superior. Os métodos setLower
e setUpper
tentam respeitar esse invariável, mas o fazem mal. Ambas setLower
e setUpper
são sequências de verificação e ação, mas não usam bloqueio suficiente para torná-las atômicas. Se o intervalo de números for mantido (0, 10) e um segmento ligar setLower(5)
enquanto outro segmento setUpper(4)
, com algum tempo azarado, ambos passarão nas verificações nos setters e as duas modificações serão aplicadas. O resultado é que o intervalo agora mantém (5, 4) - um estado inválido . Portanto, enquanto os AtomicIntegers subjacentes são seguros para threads, a classe composta não é . Porque as variáveis de estado subjacentes lower
eupper
não são independentes, NumberRange
não podem simplesmente delegar a segurança do encadeamento às suas variáveis de estado seguras.
NumberRange
pode ser tornado seguro para threads usando o bloqueio para manter seus invariantes, como proteger as partes inferior e superior com uma trava comum. Ele também deve evitar a publicação inferior e superior para impedir que os clientes subvertam seus invariantes.
Se uma classe tiver ações compostas NumberRange
, a delegação sozinha não é novamente uma abordagem adequada para a segurança do encadeamento. Nesses casos, a classe deve fornecer seu próprio bloqueio para garantir que as ações compostas sejam atômicas, a menos que toda a ação composta também possa ser delegada às variáveis de estado subjacentes.
Se uma classe for composta de várias variáveis de estado seguras de encadeamento independentes e não tiver operações que possuam transições de estado inválidas, poderá delegar segurança de encadeamento às variáveis de estado subjacentes.