ConcurrentModificationException e os fail-fast iterators

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.

23 Comentários

  1. caxqueiroz 18/08/2010 at 10:29 #

    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/08/2010 at 10:48 #

    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/08/2010 at 10:51 #

    Olá,

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

    Parabéns,
    abraço

  4. Wagner 18/08/2010 at 11:04 #

    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/08/2010 at 11:20 #

    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/08/2010 at 12:42 #

    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/08/2010 at 14:17 #

    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/08/2010 at 22:45 #

    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/09/2010 at 11:26 #

    É 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/09/2010 at 11:45 #

    Muito bom…

  11. Paulo Silveira 02/09/2010 at 12:41 #

    @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/11/2011 at 16:56 #

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

  13. Alfredo 24/09/2015 at 13:31 #

    Muito bom artigo!!

  14. Guilherme Philippi 10/11/2015 at 10:06 #

    Parabéns Paulo! Excelente explicação, obrigado pela ajuda.

  15. Luiz Fernando 18/11/2015 at 10:08 #

    Muito Bom! vlw

  16. Gabriel G. 07/01/2016 at 10:33 #

    Cara aqui não funcionou… estou percorrendo uma lista cujo vou apagando todos os itens da mesma até encontrar o elemento que o usuário inseriu
    public RetornoPecas ListaPecas(EntradaPecas entrada) {
    RetornoPecas retorno = new RetornoPecas();
    List listPeca = new ArrayList();
    try {
    Connection conn = DBConnection.getConnection();

    retorno = (PecasInventarioDAO.retornaProdutosFiltro(conn, entrada));
    listPeca = retorno.getListaProdutos();
    for (Iterator i = listPeca.iterator(); i.hasNext();) {
    PecaInventario peca = i.next();
    if(!peca.equals(entrada.getProduto())) {
    i.remove();
    } else {
    break;
    }
    }
    retorno.setListaProdutos(listPeca)
    fazendo desta forma, minha lista está retornando vazia… teria alguma dica??

  17. Alisson 11/01/2016 at 14:31 #

    Parabéns pelo artigo meu querido, sanou todas as minhas dores de cabeça sobre essa exception.

  18. Paulo Silveira 12/01/2016 at 01:47 #

    gabriel, posta o codigo no http://www.guj.com.br e sua duvida que a gente ve la

  19. Adriano 04/04/2016 at 00:18 #

    Muito bom o post! Parabéns, Paulo!

  20. Vinicius Oliveira 06/10/2016 at 20:04 #

    Muito obrigado, resolvi colocando um break;.

  21. Antonio Carlos Del Grande 26/10/2016 at 17:28 #

    Obrigado e parabéns pelo artigo, m ajudou muito.

  22. Augusto 04/01/2017 at 11:49 #

    Ótimo artigo, obrigado.

  23. Rogerio argiles 03/07/2018 at 14:19 #

    Otimo artigo ajudo muito!

Deixe uma resposta