Ao guardar objetos em uma coleção que é compartilhada por outras threads, é necessário tomar providências para que não sejam lançadas exceções inesperadas. A medida básica é garantir que os pontos de acesso às coleções compartilhadas devem obter exclusividade sobre seu uso com a diretiva synchronized. Veja abaixo um exemplo de classe que torna o uso de uma lista à prova de erros de concorrência:
public class ListaSegura {
private List lista = new ArrayList();
public void adiciona(Object objeto) {
synchronized (lista) {
lista.add(objeto);
}
}
public void remove(Object objeto) {
synchronized (lista) {
lista.remove(objeto);
}
}
public void listar() {
synchronized (lista) {
for (Object o : lista) {
System.out.println(o);
}
}
}
}
É uma solução simples e que resolve em parte o problema. Contudo na maioria das vezes não podemos obter a exclusividade para iterar sobre uma coleção. No caso mostrado acima isso é possível pois estamos apenas imprimindo o objeto. Mas existem alguns casos em que obter essa exclusividade para a iteração nos traria alguns problemas. Esses casos estão descritos abaixo:
1- Alteração da coleção: No caso de você iterar pela coleção para remover ou adicionar algum objeto a ela. A exclusão ou adição ocorreria no meio da iteração e com isso seria lançada uma exceção de concorrência.Um exemplo simples disso são classes que gerenciam cache e precisam periodicamente remover objetos expirados.
2- Iteração prolongada: Quando a coleção possui objetos demais ou o processo executado durante a iteração é lento, tornando o tempo de exclusividade total muito longo. Isso faria com que o restante do sistema que precisasse utilizar essa coleção ficasse muito tempo aguardando pela exclusividade terminar. Um exemplo comum é a execução de métodos que acessem banco de dados dentro da iteração de um objeto exclusivo.
Para resolver esses dois casos identifiquei o padrão de desenvolvimento Cópia Exclusiva de Coleção. A idéia é obter a exclusividade da lista somente para fazer uma cópia dos itens em outra coleção e então poder iterar sobre esta cópia sem muitas preocupações. Abaixo mostro um exemplo de uma classe Armario que possui muitas Coisas. Se alguma coisa for lixo, ela deve ser removida na execução do método removeLixo.
public class Armario {
private Listcoisas = new LinkedList ();
public void removeLixo() {
Listcopia = lista();
for (Coisa coisa : copia) {
if (coisa.isLixo())
remove(coisa);
}
}
public Listlista() {
Listcopia = null;
synchronized (coisas) {
copia = new ArrayList(coisas.size());
copia.addAll(coisas);
}
return copia;
}
public void adiciona(Coisa coisa) {
synchronized (coisas) {
coisas.add(coisa);
}
}
public void remove(Coisa coisa) {
synchronized (coisas) {
coisas.remove(coisa);
}
}
}
Com isso o tempo de exclusividade fica restrito ao tempo da cópia para a outra coleção, que ocorre no método lista. Dessa forma temos uma folga no tempo de iteração total e a possibilidade de rearranjar a coleção, adicionando ou removendo itens a ela. É importante ressaltar que o melhor jeito de evitar o calafrio de receber um java.util.ConcurrentModificationException é evitar a utilização de objetos compartilhados entre threads. Quando isso não é possível, o jeito é utilizar um padrão como Cópia Exclusiva de Coleção.