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
fonte
AtomicLong
aposta 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 umaHashMap
fábrica sincronizada e provavelmente verá uma melhoria, a menos que tudo o que você faça nesses threads seja a instanciação do mapa.Respostas:
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.
fonte
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
Random
usado 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
Random
nã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
HashMap
eliminaria 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
ConcurrentHashMap
etc.fonte
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.
fonte
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 ).fonte