É um thread-safe HashMap para chaves diferentes?

87

Se eu tiver dois threads múltiplos acessando um HashMap, mas garantir que eles nunca acessem a mesma chave ao mesmo tempo, isso ainda pode levar a uma condição de corrida?

Helder S Ribeiro
fonte

Respostas:

99

Na resposta de @ dotsid, ele diz o seguinte:

Se você alterar um HashMap de qualquer forma, seu código será simplesmente quebrado.

Ele está correto. Um HashMap atualizado sem sincronização será interrompido, mesmo se os threads estiverem usando conjuntos de chaves separados. Aqui estão algumas das coisas que podem dar errado.

  • Se um thread faz um put, então outro thread pode ver um valor obsoleto para o tamanho do hashmap.

  • Quando um thread faz um putque dispara uma reconstrução da tabela, outro thread pode ver versões temporárias ou obsoletas da referência de matriz de hashtable, seu tamanho, seu conteúdo ou as cadeias de hash. O caos pode acontecer.

  • Quando um encadeamento faz um putpara uma chave que colide com alguma chave usada por algum outro encadeamento e o último encadeamento faz um putpara sua chave, o último pode ver uma cópia desatualizada da referência da cadeia hash. O caos pode acontecer.

  • Quando um thread investiga a mesa com uma chave que colide com uma das chaves de outro thread, ele pode encontrar essa chave na cadeia. Ele chamará equals nessa chave e, se os threads não estiverem sincronizados, o método equals pode encontrar um estado obsoleto nessa chave.

E se você tem dois tópicos simultaneamente fazendo putou removepedidos, existem inúmeras oportunidades para condições de corrida.

Posso pensar em três soluções:

  1. Use um ConcurrentHashMap.
  2. Use um regular, HashMapmas sincronize do lado de fora; por exemplo, usando mutexes primitivos, Lockobjetos, etc.
  3. Use um diferente HashMappara cada tópico. Se os encadeamentos realmente tiverem um conjunto de chaves separado, não haverá necessidade (de uma perspectiva algorítmica) de compartilhar um único Mapa. Na verdade, se seus algoritmos envolvem os threads que iteram as chaves, valores ou entradas do mapa em algum ponto, a divisão de um único mapa em vários mapas pode fornecer um aumento significativo de velocidade para essa parte do processamento.
Stephen C
fonte
30

Basta usar um ConcurrentHashMap. O ConcurrentHashMap usa vários bloqueios que abrangem uma variedade de depósitos de hash para reduzir as chances de um bloqueio ser contestado. Há um impacto marginal de desempenho na aquisição de um bloqueio não contestado.

Para responder à sua pergunta original: De acordo com o javadoc, contanto que a estrutura do mapa não mude, você está bem. Isso significa nenhuma remoção de elementos e nenhuma adição de novas chaves que ainda não estão no mapa. Substituir o valor associado às chaves existentes é adequado.

Se vários threads acessam um mapa hash simultaneamente e pelo menos um dos threads modifica o mapa estruturalmente, ele deve ser sincronizado externamente. (Uma modificação estrutural é qualquer operação que adiciona ou exclui um ou mais mapeamentos; simplesmente alterar o valor associado a uma chave que uma instância já contém não é uma modificação estrutural.)

Embora não dê nenhuma garantia sobre a visibilidade. Portanto, você deve estar disposto a aceitar a recuperação de associações obsoletas ocasionalmente.

Tim Bender
fonte
6

Depende do que você quer dizer com "acessar". Se você apenas estiver lendo, poderá ler até mesmo as mesmas chaves, desde que a visibilidade dos dados seja garantida pelas regras " acontece antes ". Isso significa que HashMapnão deve mudar e todas as alterações (construções iniciais) devem ser concluídas antes que qualquer leitor comece a acessar HashMap.

Se você alterar um HashMapde qualquer forma, seu código será simplesmente quebrado. @Stephen C fornece uma boa explicação do porquê.

EDITAR: Se o primeiro caso for sua situação real, eu recomendo que você use Collections.unmodifiableMap()para ter certeza de que seu HashMap nunca é alterado. Objetos que são apontados por HashMapnão devem mudar também, então o uso agressivo de finalpalavras-chave pode ajudá-lo.

E, como @Lars Andren diz, ConcurrentHashMapé a melhor escolha na maioria dos casos.

Denis Bazhenov
fonte
2
ConcurrentHashMap é a melhor escolha na minha opinião. A única razão pela qual eu não recomendei foi porque o autor não perguntou :) Tem menos throughput por causa das operações CAS, mas como diz a regra de ouro da programação simultânea: "Faça certo e só então torne-o rápido ":)
Denis Bazhenov,
unmodifiableMapgarante que o cliente não pode alterar o mapa. Não faz nada para garantir que o mapa subjacente não seja alterado.
Pete Kirkham,
Como já indiquei: "Objetos apontados por HashMap não devem mudar também"
Denis Bazhenov,
4

Modificar um HashMap sem a sincronização adequada de dois threads pode facilmente levar a uma condição de corrida.

  • Quando um put()leva a um redimensionamento da tabela interna, isso leva algum tempo e o outro thread continua a gravar na tabela antiga.
  • Dois put()para chaves diferentes levam a uma atualização do mesmo intervalo se os hashcodes das chaves forem iguais ao módulo do tamanho da tabela. (Na verdade, a relação entre o hashcode e o índice do bucket é mais complicada, mas ainda podem ocorrer colisões.)
Christian Semrau
fonte
1
É pior do que apenas as condições de corrida. Dependendo da parte interna da HashMapimplementação que você está usando, você pode obter a corrupção das HashMapestruturas de dados, etc., causada por anomalias de memória.
Stephen C