Há um caso em que um mapa será construído e, uma vez inicializado, nunca será modificado novamente. No entanto, ele será acessado (apenas por meio de get (chave)) a partir de vários threads. É seguro usar um java.util.HashMap
dessa maneira?
(Atualmente, estou felizmente usando um java.util.concurrent.ConcurrentHashMap
e não tenho necessidade de melhorar o desempenho, mas estou simplesmente curioso para HashMap
saber se um simples seria suficiente. Portanto, essa pergunta não é "Qual deles devo usar?" Nem é uma questão de desempenho. Em vez disso, a pergunta é "Seria seguro?")
java
multithreading
concurrency
hashmap
Dave L.
fonte
fonte
ClassLoader
. Isso vale por uma questão separada por si só. Eu ainda o sincronizaria e o perfil explicitamente para verificar se estava causando problemas reais de desempenho.Map
, a qual você não explicou. Se você não fizer isso com segurança, não é seguro. Se você fizer isso com segurança, é . Detalhes na minha resposta.Respostas:
Seu idioma está seguro se e somente se a referência ao
HashMap
for publicada com segurança . Em vez de qualquer coisa relacionada aos internosHashMap
, a publicação segura lida com como o thread de construção torna a referência ao mapa visível para outros threads.Basicamente, a única corrida possível aqui é entre a construção do
HashMap
e qualquer thread de leitura que possa acessá-lo antes de ser totalmente construído. A maior parte da discussão é sobre o que acontece com o estado do objeto do mapa, mas isso é irrelevante, pois você nunca o modifica - portanto, a única parte interessante é como aHashMap
referência é publicada.Por exemplo, imagine que você publique o mapa assim:
... e em algum momento
setMap()
é chamado com um mapa, e outros threads estão usandoSomeClass.MAP
para acessar o mapa e verifique se há nulos como este:Isso não é seguro , embora provavelmente pareça ser. O problema é que não há relação de antes do acontecimento entre o conjunto de
SomeObject.MAP
e a leitura subsequente em outro encadeamento, portanto, o encadeamento de leitura fica livre para ver um mapa parcialmente construído. Isso pode praticamente fazer qualquer coisa e, mesmo na prática, faz coisas como colocar o thread de leitura em um loop infinito .Para publicar o mapa com segurança, você precisa estabelecer uma relação de antes do acontecimento entre a escrita da referência à
HashMap
(ou seja, a publicação ) e os leitores subsequentes dessa referência (ou seja, o consumo). Convenientemente, existem apenas algumas maneiras fáceis de lembrar de conseguir isso [1] :Os mais interessantes para o seu cenário são (2), (3) e (4). Em particular, (3) se aplica diretamente ao código que tenho acima: se você transformar a declaração de
MAP
para:então tudo é kosher: os leitores que veem um valor não nulo necessariamente têm um relacionamento de antes da loja
MAP
e, portanto, veem todas as lojas associadas à inicialização do mapa.Os outros métodos alteram a semântica do seu método, pois ambos (2) (usando o inicializador estático) e (4) (usando final ) implicam que você não pode definir
MAP
dinamicamente no tempo de execução. Se você não precisar fazer isso, basta declararMAP
comostatic final HashMap<>
e você terá uma publicação segura.Na prática, as regras são simples para acesso seguro a "objetos nunca modificados":
Se você estiver publicando um objeto que não é inerentemente imutável (como em todos os campos declarados
final
) e:final
campo (inclusivestatic final
para membros estáticos).É isso aí!
Na prática, é muito eficiente. O uso de um
static final
campo, por exemplo, permite que a JVM assuma o valor inalterado durante a vida útil do programa e o otimize fortemente. O uso de umfinal
campo membro permite que a maioria das arquiteturas leia o campo de maneira equivalente a uma leitura normal do campo e não inibe outras otimizações c .Finalmente, o uso de
volatile
tem algum impacto: nenhuma barreira de hardware é necessária em muitas arquiteturas (como x86, especificamente aquelas que não permitem que as leituras passem por leituras), mas algumas otimizações e reordenações podem não ocorrer no tempo de compilação - mas isso efeito é geralmente pequeno. Em troca, você realmente obtém mais do que solicitou - não apenas pode publicar com segurança umHashMap
, mas também pode armazenar tantos outrosHashMap
s não modificados quanto desejar na mesma referência e ter certeza de que todos os leitores verão um mapa publicado com segurança .Para mais detalhes, consulte Shipilev ou esta FAQ de Manson e Goetz .
[1] Citando diretamente do shipilev .
a Isso parece complicado, mas o que quero dizer é que você pode atribuir a referência no momento da construção - no ponto da declaração ou no construtor (campos de membros) ou no inicializador estático (campos estáticos).
b Opcionalmente, você pode usar um
synchronized
método para obter / definir, ou umAtomicReference
ou algo assim, mas estamos falando do trabalho mínimo que você pode fazer.c Algumas arquiteturas com modelos de memória muito fracos (estou olhando para você , Alpha) podem exigir algum tipo de barreira de leitura antes de uma
final
leitura - mas elas são muito raras hoje em dia.fonte
never modify
HashMap
não significa que ostate of the map object
thread é seguro, eu acho. Deus conhece a implementação da biblioteca, se o documento oficial não diz que é seguro para threads.get()
pode, de fato, executar algumas gravações, por exemplo, atualizar algumas estatísticas (ou no caso deLinkedHashMap
atualização ordenada por acesso da ordem de acesso). Assim, uma classe bem escrito deve fornecer alguma documentação que deixa claro se ...const
é realmente somente leitura nesse sentido (internamente, eles ainda podem executar gravações, mas terão que ser protegidos contra threads). Não háconst
palavra-chave em Java e não conheço nenhuma garantia geral documentada, mas, em geral, as classes de biblioteca padrão se comportam conforme o esperado e as exceções são documentadas (veja oLinkedHashMap
exemplo em que operações como ROget
são explicitamente mencionadas como não seguras).HashMap
na documentação, temos o comportamento de segurança de thread dessa classe: se vários threads acessam um mapa de hash simultaneamente e pelo menos um deles modifica estruturalmente o mapa, deve ser sincronizado externamente. (A modificação estrutural é qualquer operação que adiciona ou elimina um ou mais mapeamentos; meramente mudando o valor associado com uma chave que um exemplo já contém não é uma modificação estrutural.)HashMap
métodos que esperamos serem somente leitura, são somente leitura, pois eles não modificam estruturalmente oHashMap
. Obviamente, essa garantia pode não seMap
aplicar a outras implementações arbitrárias , mas a questão éHashMap
especificamente.Jeremy Manson, o deus no que diz respeito ao modelo de memória Java, possui um blog de três partes sobre esse tópico - porque, em essência, você está fazendo a pergunta "É seguro acessar um HashMap imutável" - a resposta é sim. Mas você deve responder o predicado para a pergunta que é - "O meu HashMap é imutável". A resposta pode surpreendê-lo - o Java possui um conjunto de regras relativamente complicado para determinar a imutabilidade.
Para mais informações sobre o tópico, leia as postagens no blog de Jeremy:
Parte 1 sobre Imutabilidade em Java: http://jeremymanson.blogspot.com/2008/04/immutability-in-java.html
Parte 2 sobre Imutabilidade em Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html
Parte 3 sobre Imutabilidade em Java: http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-3.html
fonte
As leituras são seguras do ponto de vista da sincronização, mas não do ponto de vista da memória. Isso é algo que é amplamente mal compreendido entre os desenvolvedores Java, incluindo aqui no Stackoverflow. (Observe a classificação desta resposta como prova.)
Se você tiver outros threads em execução, eles poderão não ver uma cópia atualizada do HashMap se não houver gravação de memória no thread atual. As gravações na memória ocorrem através do uso de palavras-chave sincronizadas ou voláteis, ou através do uso de algumas construções de simultaneidade java.
Veja o artigo de Brian Goetz sobre o novo Java Memory Model para obter detalhes.
fonte
Depois de um pouco mais olhando, eu encontrei isso no doc java (grifo meu):
Isso parece implicar que será seguro, assumindo que o inverso da afirmação é verdadeiro.
fonte
Uma observação é que, em algumas circunstâncias, um get () de um HashMap não sincronizado pode causar um loop infinito. Isso pode ocorrer se um put () simultâneo causar uma nova refazer do mapa.
http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html
fonte
Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally.
Há uma reviravolta importante embora. É seguro acessar o mapa, mas, em geral, não é garantido que todos os threads vejam exatamente o mesmo estado (e, portanto, valores) do HashMap. Isso pode acontecer em sistemas multiprocessadores nos quais as modificações feitas no HashMap por um encadeamento (por exemplo, aquele que o povoou) podem ficar no cache da CPU e não serão vistas por encadeamentos em execução em outras CPUs, até que uma operação de cerca de memória seja concluída. realizada garantindo a coerência do cache. A especificação da linguagem Java é explícita nesta: a solução é adquirir um bloqueio (sincronizado (...)) que emite uma operação de cerca de memória. Portanto, se você tiver certeza de que, após preencher o HashMap, cada um dos encadeamentos adquira QUALQUER bloqueio, será possível, a partir desse ponto, acessar o HashMap a partir de qualquer encadeamento até que o HashMap seja modificado novamente.
fonte
De acordo com http://www.ibm.com/developerworks/java/library/j-jtp03304/ # Segurança de inicialização, você pode tornar seu HashMap um campo final e, após a conclusão do construtor, ele será publicado com segurança.
... Sob o novo modelo de memória, há algo semelhante a um relacionamento de antes do acontecimento entre a gravação de um campo final em um construtor e o carregamento inicial de uma referência compartilhada para esse objeto em outro encadeamento. ...
fonte
Portanto, o cenário que você descreveu é que você precisa colocar um monte de dados em um mapa e, quando terminar de preenchê-lo, trate-o como imutável. Uma abordagem que é "segura" (o que significa que você está aplicando que realmente é tratado como imutável) é substituir a referência por
Collections.unmodifiableMap(originalMap)
quando estiver pronto para torná-la imutável.Para um exemplo de como mapas ruins podem falhar se usados simultaneamente, e a solução sugerida que mencionei, confira esta entrada de parada de erros: bug_id = 6423457
fonte
Esta pergunta é abordada no livro "Java Concurrency in Practice" de Brian Goetz (Listagem 16.8, página 350):
Como
states
é declarado comofinal
e sua inicialização é realizada no construtor de classe do proprietário, qualquer thread que posteriormente ler esse mapa terá a garantia de vê-lo no momento em que o construtor for concluído, desde que nenhum outro thread tente modificar o conteúdo do mapa.fonte
Esteja avisado de que, mesmo no código de thread único, substituir um ConcurrentHashMap por um HashMap pode não ser seguro. ConcurrentHashMap proíbe nulo como uma chave ou valor. O HashMap não os proíbe (não pergunte).
Portanto, na situação improvável de que seu código existente possa adicionar um nulo à coleção durante a instalação (presumivelmente em algum tipo de falha), substituir a coleção conforme descrito alterará o comportamento funcional.
Dito isto, desde que você não faça mais nada, as leituras simultâneas de um HashMap são seguras.
[Edit: por "leituras simultâneas", quero dizer que também não há modificações simultâneas.
Outras respostas explicam como garantir isso. Uma maneira é tornar o mapa imutável, mas não é necessário. Por exemplo, o modelo de memória JSR133 define explicitamente o início de um encadeamento como uma ação sincronizada, o que significa que as alterações feitas no encadeamento A antes de iniciar o encadeamento B são visíveis no encadeamento B.
Minha intenção não é contradizer essas respostas mais detalhadas sobre o Java Memory Model. Esta resposta pretende apontar que, além dos problemas de simultaneidade, há pelo menos uma diferença de API entre o ConcurrentHashMap e o HashMap, que pode prejudicar até mesmo um programa de thread único que substitui um pelo outro.]
fonte
http://www.docjar.com/html/api/java/util/HashMap.java.html
Aqui está a fonte do HashMap. Como você pode ver, não há absolutamente nenhum código de bloqueio / mutex lá.
Isso significa que, embora seja bom ler de um HashMap em uma situação multithread, eu definitivamente usaria um ConcurrentHashMap se houvesse várias gravações.
O interessante é que o .NET HashTable e o Dictionary <K, V> criaram código de sincronização.
fonte
Se a inicialização e todas as entradas estiverem sincronizadas, você será salvo.
O código a seguir é salvo porque o carregador de classes cuidará da sincronização:
O código a seguir é salvo porque a gravação de volátil cuidará da sincronização.
Isso também funcionará se a variável do membro for final, porque final também é volátil. E se o método for um construtor.
fonte