Recientemente planteé una pregunta en stackoverflow, luego encontré la respuesta. La pregunta inicial era ¿Qué mecanismos distintos de los mutex o la recolección de basura pueden ralentizar mi programa Java de subprocesos múltiples?
Descubrí para mi horror que HashMap se ha modificado entre JDK1.6 y JDK1.7. Ahora tiene un bloque de código que hace que todos los hilos que crean HashMaps se sincronicen.
La línea de código en JDK1.7.0_10 es
/**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 termina llamando
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));
}
Buscando en otros JDK, encuentro que esto no está presente en JDK1.5.0_22 o JDK1.6.0_26.
El impacto en mi código es enorme. Hace que cuando ejecuto en 64 subprocesos, obtengo menos rendimiento que cuando ejecuto en 1 subproceso. Una JStack muestra que la mayoría de los hilos pasan la mayor parte del tiempo girando en ese bucle en Random.
Entonces parece que tengo algunas opciones:
- Reescriba mi código para que no use HashMap, pero use algo similar
- De alguna manera juegue con el rt.jar y reemplace el hashmap dentro de él
- Meterse con la ruta de clases de alguna manera, por lo que cada hilo obtiene su propia versión de HashMap
Antes de comenzar por cualquiera de estos caminos (todos parecen llevar mucho tiempo y potencialmente de alto impacto), me preguntaba si me había perdido un truco obvio. ¿Alguno de ustedes puede sugerir personas de desbordamiento de pila cuál es el mejor camino, o quizás identificar una nueva idea?
Gracias por la ayuda
fuente
AtomicLong
apuesta a que la contención de escritura funcione bien. Tiene una gran contención de escritura, por lo que necesita un bloqueo exclusivo regular. Escriba unaHashMap
fábrica sincronizada y probablemente verá una mejora, a menos que todo lo que haga en estos hilos sea la creación de instancias de mapas.Respuestas:
Soy el autor original del parche que apareció en 7u6, CR # 7118743: Hash alternativo para cadenas con mapas basados en Hash.
Reconoceré desde el principio que la inicialización de hashSeed es un cuello de botella, pero no esperábamos que fuera un problema, ya que solo ocurre una vez por instancia de Hash Map. Para que este código sea un cuello de botella, tendría que crear cientos o miles de mapas hash por segundo. Esto ciertamente no es típico. ¿Existe realmente una razón válida para que su aplicación haga esto? ¿Cuánto tiempo viven estos mapas hash?
Independientemente, probablemente investigaremos el cambio a ThreadLocalRandom en lugar de Random y posiblemente alguna variante de inicialización perezosa como sugiere cambecc.
EDITAR 3
Se envió una solución para el cuello de botella al repositorio mercurial de actualización de JDK7:
http://hg.openjdk.java.net/jdk7u/jdk7u-dev/jdk/rev/b03bbdef3a88
La solución será parte de la próxima versión 7u40 y ya está disponible en las versiones 2.4 de IcedTea.
Las versiones de prueba casi finales de 7u40 están disponibles aquí:
https://jdk7.java.net/download.html
Los comentarios aún son bienvenidos. Envíelo a http://mail.openjdk.java.net/mailman/listinfo/core-libs-dev para asegurarse de que sea visto por los desarrolladores de openJDK.
fuente
Esto parece un "error" que puede solucionar. Hay una propiedad que deshabilita la nueva función "hash alternativo":
jdk.map.althashing.threshold = -1
Sin embargo, deshabilitar el hash alternativo no es suficiente porque no desactiva la generación de una semilla de hash aleatoria (aunque realmente debería). Por lo tanto, incluso si desactiva el hash alternativo, todavía tiene contención de subprocesos durante la instanciación del mapa hash.
Una forma particularmente desagradable de solucionar esto es reemplazar a la fuerza la instancia de
Random
usada para la generación de semillas hash con su propia versión no 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 qué (probablemente) es seguro hacer esto? Debido a que el hash alternativo se ha deshabilitado, lo que hace que se ignoren las semillas hash aleatorias. Así que no importa que nuestra instancia de
Random
no sea aleatoria. Como siempre con hacks desagradables como este, utilícelo con precaución.(Gracias a https://stackoverflow.com/a/3301720/1899721 por el código que establece los campos finales estáticos).
--- Editar ---
FWIW, el siguiente cambio
HashMap
eliminaría la contención de subprocesos cuando el hash alt está deshabilitado:- 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();
Se puede utilizar un enfoque similar para
ConcurrentHashMap
, etc.fuente
Hay muchas aplicaciones que crean un HashMap transitorio por registro en aplicaciones de big data. Estos analizadores y serializadores, por ejemplo. Poner cualquier sincronización en clases de colecciones no sincronizadas es un verdadero problema. En mi opinión, esto es inaceptable y debe solucionarse lo antes posible. El cambio que aparentemente se introdujo en 7u6, CR # 7118743 debería revertirse o corregirse sin requerir ninguna sincronización u operación atómica.
De alguna manera, esto me recuerda el error colosal de sincronizar StringBuffer y Vector y HashTable en JDK 1.1 / 1.2. La gente pagó caro durante años por ese error. No es necesario repetir esa experiencia.
fuente
Suponiendo que su patrón de uso es razonable, querrá usar su propia versión de Hashmap.
Ese fragmento de código está ahí para hacer que las colisiones de hash sean mucho más difíciles de causar, evitando que los atacantes creen problemas de rendimiento ( detalles ); suponiendo que este problema ya se haya resuelto de alguna otra manera, no creo que necesite sincronización en absoluto. Sin embargo, irrelevante si usa la sincronización o no, parece que querrá usar su propia versión de Hashmap para no depender tanto de lo que JDK proporciona.
Entonces, normalmente escribes algo similar y apuntas a eso, o anulas una clase en JDK. Para hacer esto último, puede anular la ruta de clase de bootstrap con el
-Xbootclasspath/p:
parámetro. Sin embargo, hacerlo "contravendrá la licencia de código binario de Java 2 Runtime Environment" ( fuente ).fuente