ConcurrentModificationException e os fail-fast iterators
Postado em 18. ago, 2010 por Paulo Silveira 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.
ASSINE NOSSO RSS
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.
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.
Claudinei
18. ago, 2010
Olá,
excelente artigo, como todos que são postados por vocês.
Parabéns,
abraço
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?
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!
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.
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.
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
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??
Luiz Roberto
02. set, 2010
Muito bom…
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.
Dionisio André
26. nov, 2011
Excenlente explicação as minhas dúvidas foram-SE!!!!