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

StackOverflow https://stackoverflow.com/questions/104184

Pergunta

Há um caso em que um mapa será construído, e uma vez que é inicializado, ele nunca será modificado novamente. Terá, no entanto, ser acessado (via única get (key)) de vários segmentos. É seguro usar um java.util.HashMap desta forma?

(Atualmente, estou feliz usando um java.util.concurrent.ConcurrentHashMap, e não têm necessidade medida para melhorar o desempenho, mas simplesmente estou curioso para saber se um HashMap simples seria suficiente. Por isso, esta questão é não "Qual que eu deveria usar?" nem é uma questão de desempenho. em vez disso, a questão é 'seria seguro?')

Foi útil?

Solução

Seu idioma é seguro se e só se a referência ao HashMap é publicada em segurança . Ao invés de qualquer coisa relativa a parte interna da própria HashMap, publicação segura trata de como o segmento de construção faz com que a referência ao mapa visível para outros tópicos.

Basicamente, a única possível corrida aqui é entre a construção do HashMap e qualquer segmentos de leitura que podem acessá-lo antes de estar totalmente construído. A maior parte da discussão é sobre o que acontece com o estado do objeto do mapa, mas isso é irrelevante, uma vez que você nunca modificá-lo - então a única parte interessante é a forma como a referência HashMap é publicado.

Por exemplo, imagine que você publicar o mapa como este:

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

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

... e em algum setMap() ponto é chamado com um mapa, e outros tópicos estão usando SomeClass.MAP para acessar o mapa, e verificar se há nula assim:

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

Esta é não segura , embora ele provavelmente parece que é. O problema é que não há nenhuma acontece-antes relacionamento entre o conjunto de SomeObject.MAP ea leitura subseqüente em outro segmento, de modo que o segmento de leitura é livre para ver um mapa parcialmente construído. Isso pode muito bem fazer qualquer e até mesmo na prática, faz coisas como colocar o segmento de leitura em um loop infinito .

Para publicar com segurança o mapa, você precisa estabelecer um acontece-antes relação entre o escrita da referência para o HashMap (ou seja, o publicação ) e os leitores subsequentes de que a referência (isto é, o consumo). Convenientemente, há apenas algumas fácil de lembrar maneiras de realizar que [1] :

  1. Bolsa de Valores de referência através de um campo devidamente bloqueado ( JLS 17.4.5 )
  2. Use inicializador estático para fazer as lojas de inicialização ( JLS 12,4 )
  3. Bolsa de Valores de referência através de um campo volátil ( JLS 17.4.5 ), ou como consequência desta regra, via as classes AtomicX
  4. Inicializar o valor em um campo final ( JLS 17,5 ).

As mais interessantes para o seu cenário são (2), (3) e (4). Em particular, (3) aplica-se diretamente para o código que eu tenho acima: se você transformar a declaração de MAP a:

public static volatile HashMap<Object, Object> MAP;

então tudo é kosher: leitores que vêem um não nulo valor necessariamente ter um acontece-antes relação com a loja para MAP e, portanto, ver todas as lojas associadas com a inicialização do mapa.

Os outros métodos de alterar a semântica do seu método, uma vez que ambos (2) (usando o initalizer estática) e (4) (usando final ) implica que você não pode MAP conjunto dinamicamente durante a execução. Se não o fizer necessidade para fazer isso, então simplesmente declarar MAP como um static final HashMap<> e você é garantido publicação segura.

Na prática, as regras são simples para acesso seguro a "objectos nunca modificados":

Se você estiver publicando um objeto que não é inerentemente imutável (como em todos os campos declarou final) e:

  • Você já pode criar o objeto que será umssigned no momento da declaração a :. é só usar um campo final (incluindo static final para membros estáticos)
  • Você deseja atribuir o objeto mais tarde, após a referência já é visível:. Utilizar um campo volátil b

É isso aí!

Na prática, é muito eficiente. O uso de um campo static final, por exemplo, permite que a JVM para assumir o valor não é alterado para a vida do programa e otimizá-lo fortemente. O uso de um campo de membro final permite a maioria arquiteturas para ler o campo de forma equivalente a uma leitura de campo normal e não inibe ainda mais otimizações c .

Finalmente, o uso de volatile tem algum impacto: nenhuma barreira hardware é necessária em muitas arquiteturas (como x86, especificamente aqueles que não permitem que lê a passagem lê), mas alguns otimização e reordenação pode não ocorrer em tempo de compilação tempo - mas esse efeito é geralmente pequena. Em troca, você realmente obter mais do que o que você pediu - não só você pode publicar com segurança um HashMap, você pode armazenar tantas HashMaps mais não-modificados como você quer a mesma referência e ter a certeza de que todos os leitores vão ver um publicada com segurança mapear.

Para detalhes mais sórdidos, consulte Shipilev ou este FAQ pelo Manson e Goetz .


[1] Directamente citando shipilev .


a Isso parece complicado, mas o que quero dizer é que você pode atribuir a referência em tempo de construção - tanto no ponto de declaração ou no construtor (campos de membros) ou inicializador estático (campos estáticos) .

b Opcionalmente, você pode usar um método synchronized para obter / set, ou um AtomicReference ou algo assim, mas estamos falando sobre o trabalho mínimo que você pode fazer.

c Algumas arquiteturas com modelos de memória muito fracos (estou olhando para você , Alpha) pode exigir algum tipo de barreira leitura antes de uma final ler -. Mas estes são muito raros hoje

Outras dicas

Jeremy Manson, o deus quando se trata do modelo de memória Java, tem um blog de três partes sobre este tema - porque, em essência, você está fazendo a pergunta "É seguro acessar um HashMap imutável" - a resposta para isso é sim. Mas você deve responder o predicado para essa pergunta que é - "É o meu HashMap imutável". A resposta pode surpreendê-lo -. Java tem um conjunto relativamente complicado de regras para determinar imutabilidade

Para mais informações sobre o tema, ler posts de Jeremy:

Parte 1 na imutabilidade em Java: http://jeremymanson.blogspot.com/2008/04/immutability-in -java.html

Parte 2 na imutabilidade em Java: http://jeremymanson.blogspot.com/2008/07 /immutability-in-java-part-2.html

Parte 3 na imutabilidade em Java: http://jeremymanson.blogspot.com/2008/07 /immutability-in-java-part-3.html

A lê são seguros do ponto de vista de sincronização, mas não um ponto de vista de memória. Isso é algo que é amplamente mal compreendida entre os desenvolvedores Java, incluindo aqui na Stackoverflow. (Observe a classificação de esta resposta para a prova.)

Se você tiver outros threads em execução, eles não podem ver uma cópia atualizada do HashMap se não houver gravação de memória do thread atual. As gravações da memória ocorrer através do uso das palavras-chave sincronizados ou voláteis, ou através de usos de algumas construções java concorrência.

artigo See Brian Goetz sobre a nova Java Memory Model para detalhes.

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

Note que esta implementação não é sincronizada. Se vários tópicos acesso a um mapa de hash simultaneamente, e em pelo menos um dos fios modifica o Roteiro estruturalmente, ele deve ser sincronizado externamente. (A estrutural modificação é que qualquer operação adiciona ou exclui um ou mais mapeamentos; simplesmente mudando o valor associado com uma chave que uma instância já contém não é um estrutural modificação.)

Isto parece implicar que ele estará seguro, assumindo o inverso da afirmação não é verdadeira.

Uma nota é que, sob algumas circunstâncias, um get () de um HashMap unsynchronized pode causar um loop infinito. Isso pode ocorrer se um put concorrente () faz com que uma nova versão do mapa.

http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html

Existe uma importante embora torção. É seguro para acessar o mapa, mas em geral não é garantido que todos os tópicos vai ver exatamente o mesmo estado (e, portanto, valores) do HashMap. Isso pode acontecer em multiprocessador sistemas onde as modificações no HashMap feitas por um segmento (por exemplo, o que povoou) pode se sentar em cache que de CPU e não será visto por threads em execução em outros CPUs, até que uma operação de cerca de memória é realizada assegurar a coerência de cache. O Java Specification A linguagem é explícita em um presente: a solução é adquirir um bloqueio (sincronizado (...)) que emite uma operação de cerca de memória. Então, se você tem certeza que depois de preencher o HashMap cada um dos tópicos adquire qualquer bloqueio, então é OK a partir desse ponto de acesso a HashMap de qualquer segmento até o HashMap é modificado novamente.

De acordo com a http://www.ibm.com/developerworks/java / biblioteca / j-jtp03304 / # Inicialização de segurança que você pode fazer o seu HashMap um campo final e após o construtor termina seria publicada em segurança.

... Sob o novo modelo de memória, há algo semelhante a um acontece-antes de relação entre a gravação de um campo final em um construtor e a carga inicial de uma referência compartilhada para esse objeto em outro segmento. ...

Assim, o cenário que você descreveu é que você precisa colocar um monte de dados em um mapa, então quando você terminar de preenchê-lo você tratá-lo como imutável. Uma abordagem que é "seguro" (ou seja, você está impondo que ele realmente é tratado como imutável) é substituir a referência com Collections.unmodifiableMap(originalMap) Quando estiver pronto para torná-lo imutável.

Para um exemplo de como mal mapeia pode falhar se usado concomitantemente, e sugeriu solução que eu mencionei, confira esta entrada bug desfile: bug_id = 6423457

Esteja avisado que, mesmo em código single-threaded, substituindo um ConcurrentHashMap com um HashMap pode não ser seguro. proíbe ConcurrentHashMap nulo como uma chave ou valor. não HashMap não proibi-los (não pergunte).

Assim, na situação improvável que seu código existente pode adicionar um nulo para a coleta durante a configuração (presumivelmente em um caso de falha de algum tipo), substituindo a coleção como descrito irá alterar o comportamento funcional.

Dito isso, desde que você não fizer nada mais leituras simultâneas de um HashMap são seguros.

[Edit:. Por "leituras simultâneas", quero dizer que não há também modificações concorrentes

Outras respostas explicar como garantir isso. Uma maneira é fazer com que o mapa imutável, mas não é necessário. Por exemplo, o modelo de memória JSR133 explicitamente define iniciar um thread para ser uma ação sincronizada, o que significa que as alterações feitas no thread A antes de começar segmento B são visíveis no segmento B.

A minha intenção não é contradizer essas respostas mais detalhadas sobre o modelo de memória Java. Essa resposta se destina a assinalar que, mesmo além de problemas de concorrência, há pelo menos uma diferença API entre ConcurrentHashMap e HashMap, o que poderia inviabilizar até mesmo um programa single-threaded que substituiu um com o outro.]

http://www.docjar.com/html /api/java/util/HashMap.java.html

aqui é a fonte para HashMap. Como você pode dizer, não há absolutamente nenhum código de bloqueio / mutex lá.

Isto significa que enquanto sua aprovação para ler de um HashMap em uma situação de vários segmentos, eu definitivamente usar um ConcurrentHashMap se houvesse várias gravações.

O que é interessante é que tanto o .NET HashTable e Dictionary têm construído em código de sincronização.

Se a inicialização e cada put é sincronizado você salvar.

A seguir código é salvar porque o carregador de classe vai cuidar da sincronização:

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

}

A seguir código é salvar porque a escrita de volátil vai 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 irá funcionar se a variável de membro é final, porque final também é volátil. E se o método é um construtor.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top