ConcurrentModificationException e os fail-fast iterators

Postado em 18. ago, 2010 por em Java

A java.util.ConcurrentModificationException costuma surpreender a muitos: como uma exception com esse nome pode aparecer mesmo em uma aplicação single threaded, que não envolve concorrência alguma no acesso dessa coleção?

Para entender melhor, vale relembrar que as coleções muito antigas, como Vector e Hashtable, são thread safe, implementado através do uso do synchronized em seus métodos e iteradores (Enumeration na época). No Java 1.2, com a entrada das interfaces Collection, List, Set e Iterator, as novas implementações optaram por não ser thread safe, dado o custo de performance que o synchronized apresentava (hoje em dia é muito, muito menor), e de que a grande maioria dos casos de uso dessas estruturas não necessitavam de thread safety.

Essa novidade, das novas coleções não serem preparadas para o uso em um ambiente multi thread, poderia causar surpresas para quem não a estivesse esperando. Em vez de deixar alguém percorrer uma coleção através de um Iterator enquanto ela é modificada concorrentemente (acarretando em dados incorretos, nulls, etc) optou-se por evitar esses casos. Para isso tenta-se “adivinhar” quando houver mais de uma thread trabalhando com a mesma coleção concorrentemente (uma modificando a coleção, e outra iterando-a).

Como fizeram isso sem usar mecanismos de lock? Usando um contador!

Imagine que a cada modificação na estrutura de uma ArrayList, nós incrementamos um contador (modCount). Quando um Iterator é requisitado pelo método iterator(), esse contador é guardado como atributo (expectedModCount). Cada vez que você invocar os métodos next() e remove() (lembrando que temos apenas 3 métodos na interface Iterator), esse iterador vai checar se o contador da ArrayList é exatamente igual ao número que era esperado, isso é, tem o mesmo valor desde quando começamos a percorrer seus elementos. Caso os valores sejam diferentes, a java.util.ConcurrentModificationException é lançada, pois foi detectada uma modificação concorrente (comodification).

Mas como essa exceção pode ocorrer mesmo quando não temos outras threads acessando a mesma coleção? O problema é exatamente essa tentativa de detectar um acesso concorrente:

List<String> nomesDosAlunos = new ArrayList<String>();
// ...  popula lista com strings
for (String nome : nomesDosAlunos) {
	if (nome.equals("nome procurado")) {
		nomesDosAlunos.remove(nome);
	}
}

Esse código vai lançar ConcurrentModificationException caso encontre a String procurada:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
	at java.util.AbstractList$Itr.next(AbstractList.java:343)
	...

Isso ocorre pois o Iterator utilizado internamente nesse laço vai detectar, na próxima chamada ao seu método next(), que o número de modificações desta ArrayList é diferente de quando ele foi instanciado. Essa stacktrace pode confundir um pouco, já que o uso do enhanced for esconde a utilização do iterator, e não conseguimos ver explicitamente a invocação do método next na linha do for.

Repare que a detecção nesse caso single threaded é arbitrária para evitar casos estranhos (se removessemos o objeto da lista, o iterator deveria ainda percorre-lo?), e coleções sem fail fast iterators vão possibilitar esse tipo de remoção e outras modificações sem lançar excessões. Como então evitar essa exception nas coleções com fail fast iterators? Utilizando o iterator.remove() em vez do enhanced for:

List<String> nomesDosAlunos = new ArrayList<String>();
// ...  popula lista com strings
for (Iterator<String> i = nomesDosAlunos.iterator(); i.hasNext();) {
	String nome = i.next();
	if (nome.equals("nome procurado")) {
		i.remove();
	}
}

Esses iteradores são chamados de fail-fast por possuirem essa característica de falhar quando uma modificação concorrente é detectada. Essa é apenas uma tentativa do Iterator em encontrar possíveis bugs, e existem casos e combinações em que a modificação concorrente pode passar desapercebida por esse mecanismo, e você não deve se basear nessa exception para garantir que sua aplicação é thread safe.

E se precisarmos usar uma lista em ambiente multi thread e percorrer seu iterator enquanto a modificamos? Podemos usar a CopyOnWriteArrayList (nos casos de muita leitura e pouca escrita) ou ainda, se não for precisar de acesso aleatório aos elementos, utilizar uma Queue como a ConcurrentLinkedQueue. Utilizar o antigo Vector ou a própria ArrayList em conjunto com Collections.synchronizedList é thread safe e não vai lançar ConcurrentModificationException desde que você sincronize o uso de seus iterators:

synchronized(nomesDosAlunos) {
	for (String nome : nomesDosAlunos)
		System.out.println(nome);
}

A performance dessas diferentes escolhas pode mudar drasticamente dependendo do contexto de sua aplicação. Pode também não ser necessário utilizar uma coleção que se preocupa com thread safety: algumas vezes basta diminuir o escopo da coleção (pode não ser necessário deixá-la na session, por exemplo) ou ainda trabalhar com cópias defensivas.

Tags: , ,

12 Respostas para “ConcurrentModificationException e os fail-fast iterators”

  1. caxqueiroz

    18. ago, 2010

    Parabéns pelo artigo, bem informativo. No inicio você fala que o “custo” do synchronized ficou menor na versão atual do java? você tem algum “benchmark” disso? ou sabe dizer o quanto esta mais “leve”?

    Obrigado.

  2. Paulo Silveira

    18. ago, 2010

    Oi Carlos. Vou procurar a referencia, mas pelo que me lembro, no caso de haver apenas uma thread (isso é, da contenção ter sido desnecessária), o preço do synchronized era 50x (!!) mais caro no 1.1 do que no java 5 da Sun.

  3. Claudinei

    18. ago, 2010

    Olá,

    excelente artigo, como todos que são postados por vocês.

    Parabéns,
    abraço

  4. Wagner

    18. ago, 2010

    Parabéns Paulo, excelente post. Eu me deparei com essa exception um tempo atrás, mas daí eu percebi que não precisava pecorrer toda a minha lista para remover o objeto porque os objetos eram únicos na lista, daí eu fiz assim:

    for(Pessoa p: listaPessoa()) {
        if(p.id == 10) {
            listaPessoa.remove(p);
           break;
        }
    }
    

    E me livrei da excessão. Mas agora fiquei na dúvida. A coleção foi modificada enquanto estava sendo interada correto? O uso do break fez com que a exception não fosse lançada?

  5. Paulo Silveira

    18. ago, 2010

    Oi Wagner! Se a sua colecao era uma das que tem failfast interators, é isso mesmo: voce so nao tomou a exceção pois nao deu tempo do iterator invocar o proximo .next(). Se voce tirar o break, a exceção vai ser lançada!

  6. caxqueiroz

    18. ago, 2010

    Valeu Paulo, obrigado.
    Eu não gosto muito de fazer remoção no meio do loop. Já tive problemas com linguagens menos “sofisticadas” que o Java. Desde então eu “marco” o que quero apagar e o faço após sair do loop.

  7. Washington Botelho

    18. ago, 2010

    Muito bom Paulo,

    A muito tempo atrás precisei disso e a solução foi meio pedreira, copiando para uma nova lista todos os elementos que eu queria.

    Show de bola essa solução. ;)

  8. Otávio Garcia

    18. ago, 2010

    Excelente artigo. Se o livro seguir o mesmo caminho vai mesmo valer o investimento, Paulo.

    Não esqueçam de fazer uma noite dos autografos, para que possamos trocar uma idéia com vocês.

    Abraços

  9. Fernando

    02. set, 2010

    É uma boa pergunta…sempre crio uma coleção auxiliar, percorrro via enhanced for e vou jogando os objetos q quero remover nessa coleção auxiliar..aí no final dou apenas o removeAll da Collection…é a melhor forma??

  10. Luiz Roberto

    02. set, 2010

    Muito bom…

  11. Paulo Silveira

    02. set, 2010

    @Fernando

    Eu pessoalmente prefiro usar o iterator.remove: por ficar mais simples e usar a API como ela é sugerida para ser usada. Mas guardar tudo para remover depois nao é problema algum. tem um custo de performance maior (usando o iterator é mais rapido, ainda mais se for uma lista sem RandomAccess), mas não é algo para se preocupar se for uma ArrayList. As vezes pode ficar mais claro e expressivo remover tudo depois de percorrer os candidatos.

    O post era mais para falar dos casos de concorrencia que ele tambem detecta, mas vejo que muita gente passa pelo problema por causa de uma unica thread mesmo.

  12. Dionisio André

    26. nov, 2011

    Excenlente explicação as minhas dúvidas foram-SE!!!!

Deixar uma Resposta