É seguro obter valores de um java.util.HashMap de vários threads (sem modificação)?

138

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.HashMapdessa maneira?

(Atualmente, estou felizmente usando um java.util.concurrent.ConcurrentHashMape não tenho necessidade de melhorar o desempenho, mas estou simplesmente curioso para HashMapsaber 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?")

Dave L.
fonte
4
Muitas respostas aqui estão corretas em relação à exclusão mútua dos segmentos em execução, mas incorretas em relação às atualizações de memória. Votei para cima / para baixo de acordo, mas ainda há muitas respostas incorretas com votos positivos.
Heath Borders
@Heath Borders, se a instância a foi estaticamente inicializada e não modificável, o HashMap, deve ser segura para leitura simultânea (como outros threads não poderiam ter perdido atualizações, pois não houve atualizações), certo?
kaqqao
Se ele foi inicializado estaticamente e nunca foi modificado fora do bloco estático, pode ser que seja bom, porque toda a inicialização estática é sincronizada pelo 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.
Heath Borders
@HeathBorders - o que você quer dizer com "atualizações de memória"? A JVM é um modelo formal que define coisas como visibilidade, atomicidade, relacionamentos que acontecem antes , mas não usa termos como "atualizações de memória". Você deve esclarecer, de preferência usando a terminologia do JLS.
BeeOnRope
2
@ Dave - Presumo que você ainda não esteja procurando resposta após oito anos, mas, para constar, a principal confusão em quase todas as respostas é que elas se concentram nas ações que você executa no objeto do mapa . Você já explicou que nunca modifica o objeto, e isso é irrelevante. A única "pegadinha" em potencial é a maneira como você publica a referência à 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.
BeeOnRope

Respostas:

55

Seu idioma está seguro se e somente se a referência ao HashMapfor publicada com segurança . Em vez de qualquer coisa relacionada aos internos HashMap, 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 HashMape 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 a HashMapreferência é publicada.

Por exemplo, imagine que você publique o mapa assim:

class SomeClass {
   public static HashMap<Object, Object> MAP;

   public synchronized static setMap(HashMap<Object, Object> m) {
     MAP = m;
   }
}

... e em algum momento setMap()é chamado com um mapa, e outros threads estão usando SomeClass.MAPpara acessar o mapa e verifique se há nulos como este:

HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
  .. use the map
} else {
  .. some default behavior
}

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.MAPe 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] :

  1. Troque a referência por um campo bloqueado corretamente ( JLS 17.4.5 )
  2. Use o inicializador estático para fazer os armazenamentos de inicialização ( JLS 12.4 )
  3. Troque a referência por meio de um campo volátil ( JLS 17.4.5 ), ou como conseqüência desta regra, pelas classes AtomicX
  4. Inicialize o valor em um campo final ( JLS 17.5 ).

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 MAPpara:

public static volatile HashMap<Object, Object> MAP;

então tudo é kosher: os leitores que veem um valor não nulo necessariamente têm um relacionamento de antes da loja MAPe, 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 MAPdinamicamente no tempo de execução. Se você não precisar fazer isso, basta declarar MAPcomo static 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:

  • Você já pode criar o objeto que será atribuído no momento da declaração a : basta usar um finalcampo (inclusive static finalpara membros estáticos).
  • Você deseja atribuir o objeto posteriormente, depois que a referência já estiver visível: use um campo volátil b .

É isso aí!

Na prática, é muito eficiente. O uso de um static finalcampo, por exemplo, permite que a JVM assuma o valor inalterado durante a vida útil do programa e o otimize fortemente. O uso de um finalcampo 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 volatiletem 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 um HashMap, mas também pode armazenar tantos outros HashMaps 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 synchronizedmétodo para obter / definir, ou um AtomicReferenceou 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 finalleitura - mas elas são muito raras hoje em dia.

BeeOnRope
fonte
never modify HashMapnão significa que o state of the map objectthread é seguro, eu acho. Deus conhece a implementação da biblioteca, se o documento oficial não diz que é seguro para threads.
Jiang YD
@ JiangYD - você está certo, há uma área cinza em alguns casos: quando dizemos "modificar", o que realmente queremos dizer é qualquer ação que execute internamente algumas gravações que possam correr com leituras ou gravações em outros segmentos. Essas gravações podem ser detalhes internos da implementação; portanto, mesmo uma operação que parece "somente leitura" get()pode, de fato, executar algumas gravações, por exemplo, atualizar algumas estatísticas (ou no caso de LinkedHashMapatualização ordenada por acesso da ordem de acesso). Assim, uma classe bem escrito deve fornecer alguma documentação que deixa claro se ...
BeeOnRope
... aparentemente, as operações "somente leitura" são realmente somente leitura internamente no sentido de segurança da thread. Na biblioteca padrão do C ++, por exemplo, existe uma regra geral de que a função de membro marcada consté realmente somente leitura nesse sentido (internamente, eles ainda podem executar gravações, mas terão que ser protegidos contra threads). Não há constpalavra-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 o LinkedHashMapexemplo em que operações como RO getsão explicitamente mencionadas como não seguras).
BeeOnRope
@JiangYD - finalmente, voltando à sua pergunta original, pois HashMapna 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.)
BeeOnRope
Portanto, para os HashMapmétodos que esperamos serem somente leitura, são somente leitura, pois eles não modificam estruturalmente o HashMap. Obviamente, essa garantia pode não se Mapaplicar a outras implementações arbitrárias , mas a questão é HashMapespecificamente.
BeeOnRope
70

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

Taylor Gautier
fonte
3
É um bom ponto, mas estou confiando na inicialização estática, durante a qual nenhuma referência escapa, portanto deve ser seguro.
Dave L.
5
Não vejo como essa é uma resposta altamente classificada (ou mesmo uma resposta). Por um lado, nem sequer responde à pergunta e não menciona o único princípio-chave que decidirá se é seguro ou não: publicação segura . A "resposta" se resume a "é complicado" e aqui estão três links (complexos) que você pode ler.
BeeOnRope
Ele responde a pergunta no final da primeira frase. Em termos de resposta, ele está levantando o ponto de que a imutabilidade (mencionada no primeiro parágrafo da pergunta) não é direta, juntamente com recursos valiosos que explicam mais esse tópico. Os pontos não medem se é uma resposta, mas se a resposta foi "útil" para os outros. A resposta que está sendo aceita significa que foi a resposta que o OP estava procurando e que sua resposta recebeu.
Jesse
@ Jessé, ele não está respondendo à pergunta no final da primeira frase, ele está respondendo à pergunta "é seguro acessar um objeto imutável", que pode ou não se aplicar à pergunta do OP, como ele aponta na próxima frase. Essencialmente, essa é uma resposta quase do tipo "vá descobrir você mesmo", que não é uma boa resposta para SO. Quanto aos votos positivos, acho que é mais uma função ter 10,5 anos e um tópico pesquisado com frequência. Ele recebeu apenas algumas poucas votações líquidas nos últimos anos, então talvez as pessoas estejam chegando :).
BeeOnRope 20/05/19
35

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.

Heath Borders
fonte
Desculpe pela submissão dupla Heath, só notei a sua depois que enviei a minha. :)
Alexander
2
Estou contente por haver outras pessoas aqui que realmente entendem os efeitos da memória.
Heath Borders
1
De fato, embora nenhum thread veja o objeto antes de ser inicializado corretamente, não acho que seja uma preocupação nesse caso.
Dave L.
1
Isso depende inteiramente de como o objeto é inicializado.
22416 Bill Michell
1
A pergunta diz que, uma vez que o HashMap foi inicializado, ele não pretende atualizá-lo mais. A partir de então, ele só quer usá-lo como uma estrutura de dados somente leitura. Eu acho que seria seguro fazê-lo, desde que os dados armazenados em seu mapa sejam imutáveis.
precisa saber é o seguinte
9

Depois de um pouco mais olhando, eu encontrei isso no doc java (grifo meu):

Observe que esta implementação não está sincronizada. Se vários encadeamentos acessarem um mapa de hash simultaneamente e pelo menos um dos encadeamentos modificar o mapa estruturalmente, ele deverá ser sincronizado externamente. (Uma modificação estrutural é qualquer operação que adiciona ou exclui um ou mais mapeamentos; apenas alterar o valor associado a uma chave que uma instância já contém não é uma modificação estrutural.)

Isso parece implicar que será seguro, assumindo que o inverso da afirmação é verdadeiro.

Dave L.
fonte
1
Embora esse seja um excelente conselho, como outras respostas indicam, há uma resposta mais sutil no caso de uma instância de mapa imutável e publicada com segurança. Mas você deve fazer isso apenas se você souber o que está fazendo.
Alex Miller
1
Felizmente, com perguntas como essas, mais de nós podem saber o que estamos fazendo.
Dave L.
Isso não está realmente correto. Como as outras respostas afirmam, deve haver um acontecimento anterior entre a última modificação e todas as leituras subsequentes "thread safe". Normalmente, isso significa que você deve publicar com segurança o objeto depois que ele foi criado e suas modificações são feitas. Veja a primeira resposta correta marcada.
markspace
9

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

Alex Miller
fonte
1
Na verdade, eu já vi isso pendurar a JVM sem consumir CPU (que é talvez pior)
Peter Lawrey
2
Eu acho que esse código foi reescrito de forma que não é mais possível obter o loop infinito. Mas você ainda não deve estar recebendo e retirando de um HashMap não sincronizado por outros motivos.
Alex Miller
@AlexMiller, além de outros motivos (presumo que você esteja se referindo à publicação segura), não acho que uma alteração na implementação deva ser um motivo para diminuir as restrições de acesso, a menos que seja explicitamente permitido pela documentação. Por acaso, o HashMap Javadoc para Java 8 ainda contém este aviso: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.
shmosel
8

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.

Alexander
fonte
Não tenho certeza de que o thread que o acessa adquirirá algum bloqueio, mas tenho certeza de que eles não obterão uma referência ao objeto até que ele tenha sido inicializado, portanto, não acho que eles possam ter uma cópia obsoleta.
Dave L.
@Alex: A referência ao HashMap pode ser volátil para criar as mesmas garantias de visibilidade da memória. @ Dave: Ele é possível ver referências a novos objs antes do trabalho de seu ctor se torna visível para a sua discussão.
21430 Chris Vest
@ Christian No caso geral, certamente. Eu estava dizendo que neste código, não é.
Dave L.
A aquisição de um bloqueio RANDOM não garante que todo o cache da CPU do thread seja limpo. Depende da implementação da JVM e provavelmente não será feito dessa maneira.
1515 Pierre Pierre
Eu concordo com Pierre, não acho que adquirir uma trava seja suficiente. Você precisa sincronizar no mesmo bloqueio para que as alterações se tornem visíveis.
damluar
5

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. ...

bodrin
fonte
Esta resposta é de baixa qualidade, é a mesma resposta de @taylor gauthier, mas com menos detalhes.
12406 Snicolas
1
Ummmm ... não para ser um idiota, mas você tem isso ao contrário. Taylor disse "não, vá ver esta postagem do blog, a resposta pode surpreendê-lo", enquanto essa resposta realmente acrescenta algo novo que eu não sabia ... Sobre uma relação de antes de acontecer da gravação de um campo final em um construtor. Essa resposta é excelente e fico feliz por ler.
Ajax
Hã? Esta é a única resposta correta que encontrei depois de percorrer as respostas mais bem classificadas. A chave é publicada com segurança e esta é a única resposta que até a menciona.
BeeOnRope
3

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

Vai
fonte
2
Isso é "seguro", pois reforça a imutabilidade, mas não trata da questão de segurança do encadeamento. Se o mapa é seguro para acessar com o wrapper UnmodifiableMap, ele é seguro sem ele e vice-versa.
Dave L.
2

Esta pergunta é abordada no livro "Java Concurrency in Practice" de Brian Goetz (Listagem 16.8, página 350):

@ThreadSafe
public class SafeStates {
    private final Map<String, String> states;

    public SafeStates() {
        states = new HashMap<String, String>();
        states.put("alaska", "AK");
        states.put("alabama", "AL");
        ...
        states.put("wyoming", "WY");
    }

    public String getAbbreviation(String s) {
        return states.get(s);
    }
}

Como statesé declarado como finale 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.

escudero380
fonte
1

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.]

Steve Jessop
fonte
Obrigado pelo aviso, mas não há tentativas de usar valores ou chaves nulas.
Dave L.
Pensei que não haveria. Nulos nas coleções são um canto louco do Java.
Steve Jessop
Eu não concordo com esta resposta. "As leituras simultâneas de um HashMap são seguras" por si só estão incorretas. Não indica se as leituras estão ocorrendo em um mapa que é mutável ou imutável. Para ser correto deve ler-se "Concurrent lê de um HashMap imutável são seguros"
Taylor Gautier
2
Não de acordo com os artigos aos quais você mesmo vinculou: o requisito é que o mapa não seja alterado (e as alterações anteriores devem ser visíveis a todos os threads do leitor), não que seja imutável (que é um termo técnico em Java e é um condição suficiente, mas não necessária, para a segurança).
31609 Steve Jessop #
Além disso, uma observação ... a inicialização de uma classe é sincronizada implicitamente no mesmo bloqueio (sim, você pode entrar em conflito com os inicializadores de campo estático); portanto, se sua inicialização ocorrer estaticamente, seria impossível para qualquer outra pessoa vê-la antes da conclusão da inicialização, como eles teriam que ser bloqueados no método ClassLoader.loadClass no mesmo bloqueio adquirido ... E se você estiver se perguntando sobre diferentes carregadores de classes com cópias diferentes do mesmo campo, você estaria correto ... mas isso seria ortogonal ao noção de condições de corrida; campos estáticos de um carregador de classes compartilham uma cerca de memória.
Ajax
0

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.

FlySwat
fonte
2
Eu acho que existem classes nas quais a simples leitura simultânea pode causar problemas, devido ao uso interno de variáveis ​​de instância temporárias, por exemplo. Portanto, provavelmente é necessário examinar cuidadosamente a fonte, mais do que uma verificação rápida do código de bloqueio / mutex.
Dave L.
0

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:

public static final HashMap<String, String> map = new HashMap<>();
static {
  map.put("A","A");

}

O código a seguir é salvo porque a gravação de volátil cuidará da sincronização.

class Foo {
  volatile HashMap<String, String> map;
  public void init() {
    final HashMap<String, String> tmp = new HashMap<>();
    tmp.put("A","A");
    // writing to volatile has to be after the modification of the map
    this.map = tmp;
  }
}

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.

TomWolk
fonte