quinta-feira, 18 de setembro de 2008

Cópia exclusiva de coleção

Por serem de díficil identificação, problemas de concorrência acabam indo pra produção sem sequer serem notado em ambiente de testes e desenvolvimento. Erros acabam ocorrendo de maneira intermitente, causando dores de cabeça e acessos de insanidade nos desenvolvedores. Martin Fowler em seu livro Patterns of Enterprise Application Architecture identifica algumas soluções para evitar problemas de concorrência relativos a integridade de dados. Contudo há um outro problema que não está catalogado neste livro: A concorrência de informações na memória.

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 List coisas = new LinkedList();
public void removeLixo() {
List copia = lista();
for (Coisa coisa : copia) {
if (coisa.isLixo())
remove(coisa);
}
}
public List lista() {
List copia = 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.

7 comentários:

  1. Tiago, não é mais fácil e interessante simplesmente usar a classe Vector? http://java.sun.com/javase/6/docs/api/java/util/Vector.html

    "Unlike the new collection implementations, Vector is synchronized."

    ResponderExcluir
  2. Realmente a classe Vector resolve os problemas sincronizando os métodos de adição, remoção e equivalentes. Contudo quando iteramos sobre ela podemos obter o mesmo erro:

    private Vector<Coisa> coisas = new Vector<Coisa>();
    ...
    for (Coisa coisa : coisas) {
    if (coisa.isLixo()) coisas.remove(coisa);
    System.out.println("removeu");
    }
    }
    ...

    Para resolver isso essa mesma classe possui um método que utiliza o padrão Cópia Exclusiva de Coleção. É o método elements(). Utilizando ele, e iterando sobre o Enumeration que ele retorna podemos remover itens do Vector e ter uma maior duração na iteração sem deixar o vetor indisponível:

    Enumeration<Coisa> e = coisas.elements();
    while( e.hasMoreElements() ) {
    Coisa coisa = e.nextElement();
    if (coisa.isLixo()) coisas.removeElement(coisa);
    }

    Ou seja, é um padrão que há muito é utilizado, mas que se esquecido pode causar muitos problemas. É muito fácil alguém fazer a iteração simples sobre o Vector ao invés de utilizar o método elements().

    Além disso o padrão serve também para outros tipos de coleção. Sets, Maps e até arrays simples. Tenho certeza que você já usou ele outrora.

    ResponderExcluir
  3. Joia...

    já tive esse problema e acabei dando uma solução diferente da proposta aqui.

    Da próxima vez vou tentar essa abordagem! :)

    ResponderExcluir
  4. Cláudio, se for possível compartilhe conosco essa solução diferente.

    ResponderExcluir
  5. Estou um pouco atrasado mas, nos métodos adicionar e remover, o objeto a ser sincronizado não deveria ser "coisas" invés de "coisa"?

    ResponderExcluir
  6. Nice article , you have indeed cover the topic with great details. I have also blogged my experience on java How Synchronization works in Java. let me know how do you find it

    ResponderExcluir