Dado que HashMaps en jdk1.6 y superior causan problemas con multi = threading, ¿cómo debo arreglar mi código?

83

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

Stave Escura
fuente
2
¿Qué te obliga a crear tantos hashmaps? ¿Que estás tratando de hacer?
fge
3
2 comentarios: 1. ConcurrentHashMap no parece usar eso, ¿podría ser una alternativa? 2. Este fragmento de código solo se llama al crear un mapa. Eso implica que está creando millones de hashmaps bajo una alta contención. ¿Eso realmente refleja una carga de producción realista?
assylias
1
En realidad, ConcurrentHashMap también usa ese método (en oracle jdk 1.7_10), pero aparentemente openJDK 7 no lo hace .
assylias
1
@assylias Deberías comprobar la última versión aquí . Este tiene tal línea de código.
Marko Topolnik
3
@StaveEscura AtomicLongapuesta 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 una HashMapfá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.
Marko Topolnik

Respuestas:

56

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.

Mike Duigou
fuente
1
Gracias por mirar en esto. Sí, realmente es necesario hacer tantos mapas: la aplicación es bastante simple, pero 100.000 personas pueden acceder a ella en un segundo, y eso significa que se pueden crear millones de mapas muy rápidamente. Por supuesto, puedo reescribirlo para que no use mapas, pero eso tiene un costo de desarrollo muy alto. Por ahora el plan de utilizar la reflexión para piratear el campo aleatorio se ve bien
Duela Escura
2
Mike, una sugerencia para una solución a corto plazo: aparte de ThreadLocalRandom (que tendrá sus propios problemas con las aplicaciones que interfieren con el almacenamiento local de subprocesos), ¿no sería mucho más fácil y barato (en términos de tiempo, riesgo y pruebas) raya Hashing.Holder.SEED_MAKER en una matriz de (digamos) <num cores> instancias aleatorias y use la identificación del hilo de llamada para% -index en ella? Esto debería aliviar instantáneamente (aunque no eliminar) la contención por hilo sin efectos secundarios notables.
Holger Hoffstätte
10
Las aplicaciones web @mduigou que tienen una alta tasa de solicitudes y usan JSON van a crear una gran cantidad de HashMaps por segundo, ya que la mayoría, si no todas, las bibliotecas JSON usan HashMaps o LinkedHashMaps para deserializar objetos JSON. Las aplicaciones web que usan JSON están muy extendidas y la creación de HashMaps puede no estar controlada por la aplicación (sino por el uso de una biblioteca), por lo que diría que existen razones válidas para no tener un cuello de botella al crear HashMaps.
sbordet
3
@mduigou quizás un alivio simple es simplemente verificar si el oldSeed es el mismo antes de llamar al CAS en él. Esta optimización (conocida como prueba-prueba y conjunto o TTAS) puede parecer redundante, pero puede tener un impacto importante en el rendimiento bajo disputa, ya que no se intenta el CAS si ya sabe que fallará. El CAS fallido tiene el desafortunado efecto secundario de establecer el estado MESI de la línea de caché en No válido, lo que requiere que todas las partes recuperen el valor de la memoria. Por supuesto, el rayado de las semillas de Holger es una excelente solución a largo plazo, pero incluso entonces se debe utilizar la optimización TTAS.
Jed Wesley-Smith
5
¿Quiere decir "cientos de miles" en lugar de "cientos o miles"? - GRAN diferencia
Michael Neale
30

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 Randomusada 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 Randomno 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 HashMapeliminarí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.

cambecc
fuente
1
Gracias. De hecho, esto es un truco, pero resuelve el problema temporalmente. Sin duda, es una solución mejor que cualquiera de la lista que identifiqué anteriormente. A largo plazo, tendré que hacer algo con un HashMap más rápido de todos modos. Esto me recuerda la solución a la antigua caché ResourceBundle que no se puede borrar. ¡El código es casi idéntico!
Stave Escura
1
Para su información, esta función de hash alternativo se describe aquí: Solicitud de revisión CR # 7118743: Hash alternativo para cadenas con mapas basados ​​en hash . Es una implementación de la función hash murmur3.
cambecc
3

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.

user1951832
fuente
2

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

eis
fuente
Ajá No me había dado cuenta de que ese era el objetivo de la optimización. Muy inteligente. Mi modelo de amenaza para los atacantes no los hace jugar con los hashmaps de esta manera, pero recordaré esto para el futuro. Estoy de acuerdo con su punto de reemplazar el HashMap eventualmente. Probablemente tendré que enhebrar un objeto de fábrica o tal vez un contenedor de IOC en cada clase que los haga. Creo que la respuesta de Cambecc me sacará del hoyo, mientras trabajo en una solución a más largo plazo
Stave Escura