Por que uma ConcurrentModificationException é lançada e como depurá-la

130

Estou usando um Collection(um HashMapusado indiretamente pelo JPA, acontece), mas aparentemente aleatoriamente o código gera um ConcurrentModificationException. O que está causando isso e como faço para corrigir esse problema? Usando alguma sincronização, talvez?

Aqui está o rastreamento de pilha completo:

Exception in thread "pool-1-thread-1" java.util.ConcurrentModificationException
        at java.util.HashMap$HashIterator.nextEntry(Unknown Source)
        at java.util.HashMap$ValueIterator.next(Unknown Source)
        at org.hibernate.collection.AbstractPersistentCollection$IteratorProxy.next(AbstractPersistentCollection.java:555)
        at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:296)
        at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:242)
        at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:219)
        at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:169)
        at org.hibernate.engine.Cascade.cascade(Cascade.java:130)
mainstringargs
fonte
1
Você pode fornecer um pouco mais de contexto? Você está mesclando, atualizando ou excluindo uma entidade? Que associações essa entidade possui? E as suas configurações em cascata?
ordnungswidrig
1
No rastreio da pilha, você pode ver que a Exceção ocorre durante a iteração no HashMap. Certamente algum outro encadeamento está modificando o mapa, mas a exceção ocorre no encadeamento que está iterando.
Chochos 31/08/10

Respostas:

263

Este não é um problema de sincronização. Isso ocorrerá se a coleção subjacente que está sendo iterada for modificada por algo diferente do próprio Iterator.

Iterator it = map.entrySet().iterator();
while (it.hasNext())
{
   Entry item = it.next();
   map.remove(item.getKey());
}

Isso lançará a ConcurrentModificationExceptionquando o it.hasNext()for chamado pela segunda vez.

A abordagem correta seria

   Iterator it = map.entrySet().iterator();
   while (it.hasNext())
   {
      Entry item = it.next();
      it.remove();
   }

Supondo que este iterador suporta a remove()operação.

Robin
fonte
1
Possivelmente, mas parece que o Hibernate está fazendo a iteração, que deve ser implementada razoavelmente corretamente. Pode haver retorno de chamada modificando o mapa, mas isso é improvável. A imprevisibilidade aponta para um problema de simultaneidade real.
Tom Hawtin - tackline
Essa exceção não tem nada a ver com a simultaneidade de encadeamento, é causada pela modificação do armazenamento de backup do iterador. Se por outro encadeamento de not não importa para o iterador. IMHO é uma exceção mal nomeada, pois fornece uma impressão incorreta da causa.
226 Robin
Concordo, no entanto, que se for imprevisível, provavelmente haverá um problema de segmentação que está causando as condições para que essa exceção ocorra. O que torna tudo mais confuso por causa do nome da exceção.
226 Robin
Isso está correto e é uma explicação melhor do que a resposta aceita, mas a resposta aceita é uma boa solução. ConcurrentHashMap não está sujeito ao CME, mesmo dentro de um iterador (embora o iterador ainda tenha sido projetado para acesso de thread único).
G__
Esta solução não faz sentido, porque o Maps não possui o método iterator (). O exemplo de Robin seria aplicável a, por exemplo, Listas.
Peter
72

Tente usar um em ConcurrentHashMapvez de um simplesHashMap

Chochos
fonte
Isso realmente resolveu o problema? Estou enfrentando o mesmo problema, mas certamente posso descartar quaisquer problemas de segmentação.
Tobiasbayer #
5
Outra solução é criar uma cópia do mapa e iterar através dessa cópia. Ou copie o conjunto de chaves e percorra-as, obtendo o valor de cada chave no mapa original.
Chochos 31/08/10
É o Hibernate que está iterando a coleção, então você não pode simplesmente copiá-la.
Tobiasbayer # 1/10
1
Salvador instantâneo. Vou analisar por que isso funcionou tão bem, para que eu não tenha mais surpresas mais adiante.
Valchris
1
Eu acho que não é problema de sincronização, é problema se a modificação da mesma modificação enquanto estiver repetindo o mesmo objeto.
Rais Alam
17

A modificação de um Collectiontempo que itera através do Collectionuso de um nãoIterator é permitida pela maioria das Collectionclasses. A biblioteca Java chama uma tentativa de modificar, Collectionenquanto iterando através dela, uma "modificação simultânea". Infelizmente, isso sugere que a única causa possível é a modificação simultânea por vários threads, mas não é assim. Usando apenas um encadeamento, é possível criar um iterador para o Collection(usando Collection.iterator()ou um loop aprimoradofor ), iniciar a iteração (usando Iterator.next()ou entrar de forma equivalente no corpo do forloop aprimorado ), modificar o Collectione continuar a iteração.

Para ajudar os programadores, algumas implementações dessas Collectionclasses tentam detectar modificações simultâneas erradas e lançam um ConcurrentModificationExceptionse detectarem. No entanto, em geral não é possível e prático garantir a detecção de todas as modificações simultâneas. Portanto, o uso incorreto do Collectionnem sempre resulta em um arremesso ConcurrentModificationException.

A documentação do ConcurrentModificationExceptiondiz:

Essa exceção pode ser lançada por métodos que detectaram a modificação simultânea de um objeto quando essa modificação não é permitida ...

Observe que essa exceção nem sempre indica que um objeto foi modificado simultaneamente por um thread diferente. Se um único encadeamento emitir uma sequência de invocações de método que viole o contrato de um objeto, o objeto poderá lançar esta exceção ...

Observe que o comportamento à prova de falhas não pode ser garantido, pois, em geral, é impossível fazer garantias concretas na presença de modificação simultânea não sincronizada. As operações à prova de falhas são realizadas ConcurrentModificationExceptioncom o melhor esforço.

Observe que

A documentação do HashSet, HashMap, TreeSete ArrayListaulas diz o seguinte:

Os iteradores retornados [direta ou indiretamente dessa classe] são rápidos: se a [coleção] for modificada a qualquer momento após a criação do iterador, de qualquer forma, exceto pelo método de remoção do próprio iterador, os Iteratorarremessos a ConcurrentModificationException. Portanto, diante da modificação simultânea, o iterador falha de maneira rápida e limpa, em vez de arriscar um comportamento arbitrário e não determinístico em um tempo indeterminado no futuro.

Observe que o comportamento à prova de falhas de um iterador não pode ser garantido, pois é, em geral, impossível fazer quaisquer garantias concretas na presença de modificação simultânea não sincronizada. Os iteradores à prova de falhas são os ConcurrentModificationExceptionmelhores esforços. Portanto, seria errado escrever um programa que dependesse dessa exceção para sua correção: o comportamento de falha rápida dos iteradores deve ser usado apenas para detectar erros .

Observe novamente que o comportamento "não pode ser garantido" e é apenas "na base do melhor esforço".

A documentação de vários métodos da Mapinterface diz o seguinte:

Implementações não simultâneas devem substituir esse método e, na melhor das hipóteses, lançar um ConcurrentModificationExceptionse for detectado que a função de mapeamento modifica esse mapa durante o cálculo. As implementações simultâneas devem substituir esse método e, na melhor das hipóteses, lançar um IllegalStateExceptionse for detectado que a função de mapeamento modifica esse mapa durante a computação e, como resultado, a computação nunca será concluída.

Observe novamente que apenas uma "base de melhor esforço" é necessária para a detecção e a ConcurrentModificationExceptioné explicitamente sugerida apenas para as classes não simultâneas (sem thread-safe).

Depuração ConcurrentModificationException

Portanto, quando você vê um rastreamento de pilha devido a ConcurrentModificationException, não é possível assumir imediatamente que a causa é o acesso multithread inseguro a um Collection. Você deve examinar o rastreamento de pilha para determinar qual classe Collectionlançou a exceção (um método da classe a lançou direta ou indiretamente) e para qual Collectionobjeto. Então você deve examinar de onde esse objeto pode ser modificado.

  • A causa mais comum é a modificação do Collectiondentro de um forloop aprimorado sobre o Collection. Só porque você não vê um Iteratorobjeto no seu código-fonte, não significa que não Iteratorexista! Felizmente, uma das declarações do forloop defeituoso geralmente estará no rastreamento da pilha; portanto, rastrear o erro geralmente é fácil.
  • Um caso mais complicado é quando seu código passa referências ao Collectionobjeto. Observe que visualizações não modificáveis de coleções (como produzidas por Collections.unmodifiableList()) mantêm uma referência à coleção modificável, portanto, a iteração sobre uma coleção "não modificável" pode gerar a exceção (a modificação foi feita em outro lugar). Outros pontos de vista do seu Collection, como sub-listas , Mapconjuntos de entrada e Mapconjuntos de chaves também reter referências ao original (modificável) Collection. Isso pode ser um problema mesmo para um thread-safe Collection, como CopyOnWriteList; não assuma que coleções (simultâneas) seguras para threads nunca podem lançar a exceção.
  • Quais operações podem modificar a Collectionpodem ser inesperadas em alguns casos. Por exemplo, LinkedHashMap.get()modifica sua coleção .
  • Os casos mais difíceis são quando a exceção ocorre devido à modificação simultânea de vários encadeamentos.

Programação para evitar erros de modificação simultâneos

Quando possível, restrinja todas as referências a um Collectionobjeto, para que seja mais fácil evitar modificações simultâneas. Crie Collectionum privateobjeto ou uma variável local e não retorne referências aos Collectionmétodos ou a seus iteradores. É então muito mais fácil examinar todos os lugares onde os itens Collectionpodem ser modificados. Se o Collectiondeve ser usado por vários encadeamentos, é prático garantir que os encadeamentos acessem o Collectionúnico com sincronização e bloqueio apropriados.

Raedwald
fonte
Gostaria de saber por que a modificação simultânea não é permitida no caso de um único thread. Quais problemas podem ocorrer se um único encadeamento puder fazer uma modificação simultânea em um mapa de hash regular?
MasterJoe 18/07
4

No Java 8, você pode usar a expressão lambda:

map.keySet().removeIf(key -> key condition);
Zentopia
fonte
2

Parece menos um problema de sincronização Java e mais um problema de bloqueio de banco de dados.

Não sei se a adição de uma versão a todas as suas classes persistentes resolverá o problema, mas é uma maneira de o Hibernate fornecer acesso exclusivo às linhas de uma tabela.

Pode ser que o nível de isolamento precise ser maior. Se você permitir "leituras sujas", talvez seja necessário aumentar para serializável.

duffymo
fonte
Eu acho que eles queriam dizer Hashtable. Ele foi enviado como parte do JDK 1.0. Como o Vector, ele foi escrito para ser seguro para threads - e lento. Ambos foram substituídos por alternativas seguras sem thread: HashMap e ArrayList. Pague pelo que você usa.
precisa saber é o seguinte
0

Tente CopyOnWriteArrayList ou CopyOnWriteArraySet, dependendo do que você está tentando fazer.

Javamann
fonte
0

Observe que a resposta selecionada não pode ser aplicada diretamente ao seu contexto antes de alguma modificação, se você estiver tentando remover algumas entradas do mapa enquanto itera o mapa como eu.

Eu apenas dou meu exemplo de trabalho aqui para iniciantes para economizar seu tempo:

HashMap<Character,Integer> map=new HashMap();
//adding some entries to the map
...
int threshold;
//initialize the threshold
...
Iterator it=map.entrySet().iterator();
while(it.hasNext()){
    Map.Entry<Character,Integer> item=(Map.Entry<Character,Integer>)it.next();
    //it.remove() will delete the item from the map
    if((Integer)item.getValue()<threshold){
        it.remove();
    }
ZhaoGang
fonte
0

Corri para essa exceção ao tentar remover x últimos itens da lista. myList.subList(lastIndex, myList.size()).clear();foi a única solução que funcionou para mim.

homem cinzento
fonte