Dado que os HashMaps em jdk1.6 e acima causam problemas com multi = threading, como devo corrigir meu código

83

Recentemente, levantei uma questão em stackoverflow e encontrei a resposta. A pergunta inicial era: quais mecanismos diferentes de mutexs ou coleta de lixo podem tornar meu programa java multi-threaded lento?

Eu descobri para meu horror que o HashMap foi modificado entre JDK1.6 e JDK1.7. Ele agora tem um bloco de código que faz com que todos os threads que criam HashMaps sejam sincronizados.

A linha de código em JDK1.7.0_10 é

 /**A randomizing value associated with this instance that is applied to hash code of  keys to make hash collisions harder to find.     */
transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);

Que acaba ligando

 protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
 }    

Procurando em outros JDKs, descobri que isso não está presente em JDK1.5.0_22 ou JDK1.6.0_26.

O impacto no meu código é enorme. Isso faz com que, quando executo em 64 threads, obtenho menos desempenho do que quando executo em 1 thread. Um JStack mostra que a maioria dos threads está gastando a maior parte do tempo girando nesse loop em Random.

Então, parece que tenho algumas opções:

  • Reescrever meu código para que eu não use HashMap, mas use algo semelhante
  • De alguma forma, mexa com o rt.jar e substitua o hashmap dentro dele
  • Mexa com o caminho da classe de alguma forma, para que cada thread obtenha sua própria versão de HashMap

Antes de começar a trilhar qualquer um desses caminhos (todos parecem muito demorados e potencialmente de alto impacto), me perguntei se não percebi um truque óbvio. Qualquer um de vocês pode sugerir o melhor caminho, ou talvez identificar uma nova ideia.

Obrigado pela ajuda

Stave Escura
fonte
2
O que requer que você crie tantos hashmaps? O que você está tentando fazer?
fge
3
2 comentários: 1. ConcurrentHashMap não parece usar isso - poderia ser uma alternativa? 2. Este trecho de código só é chamado na criação do mapa. Isso implica que você está criando milhões de hashmaps sob alta contenção - isso realmente reflete uma carga de produção realista?
Assylias
1
Na verdade, o ConcurrentHashMap também usa esse método (no oracle jdk 1.7_10) - mas aparentemente o openJDK 7 não .
Assylias
1
@assylias Você deve verificar a versão mais recente aqui . Este aqui ostenta essa linha de código.
Marko Topolnik
3
@StaveEscura AtomicLongaposta em baixa contenção de gravação para funcionar bem. Você tem alta contenção de gravação, portanto, precisa de bloqueio exclusivo regular. Escreva uma HashMapfábrica sincronizada e provavelmente verá uma melhoria, a menos que tudo o que você faça nesses threads seja a instanciação do mapa.
Marko Topolnik

Respostas:

56

Eu sou o autor original do patch que apareceu em 7u6, CR # 7118743: Hashing alternativo para string com mapas baseados em Hash‌.

Reconhecerei desde o início que a inicialização de hashSeed é um gargalo, mas não é algo que esperávamos ser um problema, já que só acontece uma vez por instância de Hash Map. Para que esse código seja um gargalo, você teria que criar centenas ou milhares de mapas de hash por segundo. Isso certamente não é típico. Existe realmente um motivo válido para seu aplicativo fazer isso? Por quanto tempo esses mapas hash vivem?

Independentemente disso, provavelmente iremos investigar a mudança para ThreadLocalRandom em vez de Random e, possivelmente, alguma variante da inicialização lenta, conforme sugerido por cambecc.

EDITAR 3

Uma correção para o gargalo foi enviada para o repositório mercurial de atualização JDK7:

http://hg.openjdk.java.net/jdk7u/jdk7u-dev/jdk/rev/b03bbdef3a88

A correção fará parte da próxima versão 7u40 e já está disponível nas versões IcedTea 2.4.

As compilações de teste quase finais de 7u40 estão disponíveis aqui:

https://jdk7.java.net/download.html

O feedback ainda é bem-vindo. Envie-o para http://mail.openjdk.java.net/mailman/listinfo/core-libs-dev para ter certeza de que será visto pelos desenvolvedores do openJDK.

Mike Duigou
fonte
1
Obrigado por investigar isso. Sim, é realmente necessário fazer tantos mapas: o aplicativo é bastante simples, mas 100.000 pessoas podem acessá-lo por segundo, e isso significa que milhões de mapas podem ser criados muito rapidamente. É claro que posso reescrever para não usar mapas, mas isso tem um custo de desenvolvimento muito alto. Por enquanto, o plano de usar reflexão para hackear o campo Random parece bom
Stave Escura
2
Mike, uma sugestão para uma correção de curto prazo: além de ThreadLocalRandom (que terá seus próprios problemas com aplicativos que bagunçam o armazenamento local de thread), não seria muito mais fácil e barato (em termos de tempo, risco e teste) stripe Hashing.Holder.SEED_MAKER em uma matriz de (digamos) <num cores> instâncias aleatórias e usar o id do thread de chamada para% -index nele? Isso deve aliviar instantaneamente (embora não eliminar) a contenção por thread, sem quaisquer efeitos colaterais perceptíveis.
Holger Hoffstätte
10
Os aplicativos da Web @mduigou que têm uma alta taxa de solicitação e usam JSON criarão um grande número de HashMaps por segundo, já que a maioria, senão todas as bibliotecas JSON, usam HashMaps ou LinkedHashMaps para desserializar objetos JSON. Os aplicativos da Web que usam JSON são amplamente difundidos e a criação de HashMaps pode não ser controlada pelo aplicativo (mas por um uso de aplicativos de biblioteca), então eu diria que há razões válidas para não haver um gargalo ao criar HashMaps.
sbordet
3
@mduigou talvez um alívio simples seja simplesmente verificar se o oldSeed é o mesmo antes de chamar o CAS nele. Essa otimização (conhecida como test-test and set ou TTAS) pode parecer redundante, mas pode ter um impacto importante no desempenho sob contenção, pois o CAS não é tentado se já sabe que irá falhar. O CAS com falha tem o infeliz efeito colateral de definir o status MESI da linha do cache como Inválido - exigindo que todas as partes recuperem o valor da memória. Claro, o striping de Holger das sementes é uma excelente solução a longo prazo, mas mesmo assim a otimização TTAS deve ser usada.
Jed Wesley-Smith
5
Você quer dizer "centenas de milhares" em vez de "centenas ou milhares"? - GRANDE diferença
Michael Neale
30

Isso parece um "bug" que você pode contornar. Existe uma propriedade que desativa o novo recurso de "hash alternativo":

jdk.map.althashing.threshold = -1

No entanto, desabilitar o hash alternativo não é suficiente porque não desativa a geração de uma semente de hash aleatória (embora realmente devesse). Portanto, mesmo se você desativar o hash alternativo, ainda haverá contenção de thread durante a instanciação do mapa hash.

Uma maneira particularmente desagradável de contornar isso é substituir à força a instância de Randomusado para geração de sementes de hash por sua própria versão não sincronizada:

// Create an instance of "Random" having no thread synchronization.
Random alwaysOne = new Random() {
    @Override
    protected int next(int bits) {
        return 1;
    }
};

// Get a handle to the static final field sun.misc.Hashing.Holder.SEED_MAKER
Class<?> clazz = Class.forName("sun.misc.Hashing$Holder");
Field field = clazz.getDeclaredField("SEED_MAKER");
field.setAccessible(true);

// Convince Java the field is not final.
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

// Set our custom instance of Random into the field.
field.set(null, alwaysOne);

Por que é (provavelmente) seguro fazer isso? Porque alt hashing foi desabilitado, fazendo com que as sementes de hash aleatórias sejam ignoradas. Portanto, não importa que nossa instância de Randomnão seja de fato aleatória. Como sempre, com hacks desagradáveis ​​como este, use com cuidado.

(Agradecimentos a https://stackoverflow.com/a/3301720/1899721 pelo código que define os campos finais estáticos).

--- Editar ---

FWIW, a seguinte alteração para HashMapeliminaria a contenção de thread quando o hash alt está desativado:

-   transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);
+   transient final int hashSeed;

...

         useAltHashing = sun.misc.VM.isBooted() &&
                 (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
+        hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0;
         init();

Uma abordagem semelhante pode ser usada para ConcurrentHashMapetc.

Cambecc
fonte
1
Obrigado. Este é realmente um hack, mas resolve o problema temporariamente. Certamente é uma solução melhor do que qualquer uma na lista que identifiquei acima. A longo prazo, vou ter que fazer algo com um HashMap mais rápido de qualquer maneira. Isso me lembra que a solução para o cache antigo do ResourceBundle não pode ser apagada. O código é quase idêntico!
Stave Escura
1
Para sua informação, este recurso de hashing alternativo é descrito aqui: Solicitação de revisão CR # 7118743: Hashing alternativo para string com mapas baseados em Hash . É uma implementação da função hash murmur3.
cambecc
3

Existem muitos aplicativos por aí que criam um HashMap temporário por registro em aplicativos de big data. Este analisadores e serializadores, por exemplo. Colocar qualquer sincronização em classes de coleções não sincronizadas é uma pegadinha. Na minha opinião, isso é inaceitável e precisa ser corrigido o mais rápido possível. A mudança que foi aparentemente introduzida no 7u6, CR # 7118743, deve ser revertida ou corrigida sem a necessidade de sincronização ou operação atômica.

De alguma forma, isso me lembra o erro colossal de fazer StringBuffer e Vector e HashTable sincronizados no JDK 1.1 / 1.2. As pessoas pagaram caro durante anos por esse erro. Não há necessidade de repetir essa experiência.

user1951832
fonte
2

Presumindo que seu padrão de uso seja razoável, você desejará usar sua própria versão do Hashmap.

Esse pedaço de código existe para tornar as colisões de hash muito mais difíceis de causar, evitando que invasores criem problemas de desempenho ( detalhes ) - presumindo que esse problema já seja tratado de alguma outra forma, não acho que você precise de sincronização. No entanto, seja irrelevante se você usa sincronização ou não, parece que você gostaria de usar sua própria versão do Hashmap para não depender muito do que o JDK fornece.

Portanto, normalmente você escreve algo semelhante e aponta para isso ou sobrescreve uma classe no JDK. Para fazer o último, você pode substituir o classpath de bootstrap com o -Xbootclasspath/p:parâmetro. Fazer isso, entretanto, "infringirá a licença do código binário do Java 2 Runtime Environment" ( fonte ).

eis
fonte
Aha. Eu não tinha percebido que esse era o objetivo da otimização. Muito esperto. Meu modelo de ameaça para invasores não permite que eles mexam em hashmaps dessa maneira, mas vou me lembrar disso no futuro. Eu concordo com seu ponto sobre a substituição do HashMap eventualmente. Provavelmente terei que encadear um objeto de fábrica ou talvez um contêiner IOC em cada classe que os torna. Acho que a resposta dada por Cambecc vai me tirar do buraco, enquanto trabalho em uma solução de longo prazo
Stave Escura