web-dev-qa-db-fra.com

Étant donné que les HashMaps dans jdk1.6 et supérieur provoquent des problèmes avec le threading multi =, comment dois-je corriger mon code

J'ai récemment posé une question dans stackoverflow, puis trouvé la réponse. La question initiale était Quels mécanismes autres que les mutex ou la récupération de place peuvent ralentir mon programme multi-thread Java?

J'ai découvert avec horreur que HashMap a été modifié entre JDK1.6 et JDK1.7. Il a maintenant un bloc de code qui synchronise tous les threads créant des HashMaps.

La ligne de code dans JDK1.7.0_10 est

 /**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);

Qui finit par appeler

 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));
 }    

En regardant dans d'autres JDK, je trouve que ce n'est pas présent dans JDK1.5.0_22 ou JDK1.6.0_26.

L'impact sur mon code est énorme. Cela fait en sorte que lorsque j'exécute sur 64 threads, j'obtiens moins de performances que lorsque j'exécute sur 1 thread. Un JStack montre que la plupart des threads passent la plupart de leur temps à tourner dans cette boucle dans Random.

Il me semble donc avoir quelques options:

  • Réécrire mon code pour ne pas utiliser HashMap, mais utiliser quelque chose de similaire
  • En quelque sorte déconner avec le rt.jar, et remplacer le hashmap à l'intérieur
  • Jouez avec le chemin de classe en quelque sorte, donc chaque thread obtient sa propre version de HashMap

Avant de commencer l'un de ces chemins (tous semblent très longs et potentiellement à fort impact), je me suis demandé si j'avais raté une astuce évidente. Est-ce que l'un d'entre vous peut empiler les débordements suggérer quel est le meilleur chemin ou peut-être identifier une nouvelle idée.

Merci pour l'aide

83
Stave Escura

Je suis l'auteur original du patch qui est apparu dans 7u6, CR # 7118743: Hashing alternatif pour String avec Hash-based Maps‌.

Je reconnais d'emblée que l'initialisation de hashSeed est un goulot d'étranglement, mais ce n'est pas un problème auquel nous nous attendions car il ne se produit qu'une fois par instance de Hash Map. Pour que ce code soit un goulot d'étranglement, vous devez créer des centaines ou des milliers de cartes de hachage par seconde. Ce n'est certainement pas typique. Y a-t-il vraiment une raison valable pour que votre application le fasse? Combien de temps ces cartes de hachage vivent-elles?

Quoi qu'il en soit, nous étudierons probablement le passage à ThreadLocalRandom plutôt qu'à Random et éventuellement une variante de l'initialisation paresseuse comme suggéré par cambecc.

EDIT 3

Un correctif pour le goulot d'étranglement a été inséré dans le dépôt Mercurial de mise à jour JDK7:

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

Le correctif fera partie de la prochaine version 7u40 et est déjà disponible dans les versions IcedTea 2.4.

Des versions de test presque finales de 7u40 sont disponibles ici:

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

Les commentaires sont toujours les bienvenus. Envoyez-le à http://mail.openjdk.Java.net/mailman/listinfo/core-libs-dev pour être sûr qu'il sera vu par les développeurs openJDK.

56
Mike Duigou

Cela ressemble à un "bug" que vous pouvez contourner. Il existe une propriété qui désactive la nouvelle fonctionnalité de "hachage alternatif":

jdk.map.althashing.threshold = -1

Cependant, la désactivation du hachage alternatif n'est pas suffisante car elle ne désactive pas la génération d'une graine de hachage aléatoire (bien qu'elle le devrait vraiment). Ainsi, même si vous désactivez le hachage alt, vous avez toujours un conflit de thread pendant l'instanciation de la carte de hachage.

Une façon particulièrement désagréable de contourner ce problème consiste à remplacer de force l'instance de Random utilisée pour la génération de graines de hachage par votre propre version non synchronisée:

// 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);

Pourquoi est-ce (probablement) sûr de le faire? Parce que le hachage alt a été désactivé, entraînant l'ignorance des graines de hachage aléatoires. Donc peu importe que notre instance de Random ne soit en fait pas aléatoire. Comme toujours avec les hacks désagréables comme celui-ci, veuillez utiliser avec prudence.

(Merci à https://stackoverflow.com/a/3301720/1899721 pour le code qui définit les champs finaux statiques).

--- Éditer ---

FWIW, la modification suivante de HashMap éliminerait la contention du thread lorsque le hachage alt est désactivé:

-   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();

Une approche similaire peut être utilisée pour ConcurrentHashMap, etc.

30
cambecc

Il existe de nombreuses applications qui créent un HashMap transitoire par enregistrement dans les applications de Big Data. Ces analyseurs et sérialiseurs, par exemple. Mettre toute synchronisation dans des classes de collections non synchronisées est un vrai problème. À mon avis, cela est inacceptable et doit être corrigé dès que possible. Le changement apparemment introduit dans 7u6, CR # 7118743 doit être annulé ou corrigé sans nécessiter de synchronisation ni d'opération atomique.

D'une certaine manière, cela me rappelle l'erreur colossale de synchroniser StringBuffer et Vector et HashTable dans JDK 1.1/1.2. Les gens ont payé cher pendant des années pour cette erreur. Pas besoin de répéter cette expérience.

3
user1951832

En supposant que votre modèle d'utilisation est raisonnable, vous voudrez utiliser votre propre version de Hashmap.

Ce morceau de code est là pour rendre les collisions de hachage beaucoup plus difficiles à provoquer, empêchant les attaquants de créer des problèmes de performances ( détails ) - en supposant que ce problème est déjà traité d'une autre manière, je ne pense pas que vous aurais besoin de synchronisation du tout. Cependant, peu importe que vous utilisiez la synchronisation ou non, il semble que vous souhaitiez utiliser votre propre version de Hashmap afin de ne pas dépendre autant de ce que JDK fournit.

Donc, soit vous écrivez normalement quelque chose de similaire et vous pointez dessus, soit vous remplacez une classe en JDK. Pour ce dernier, vous pouvez remplacer bootstrap classpath avec -Xbootclasspath/p: paramètre. Cela contreviendra toutefois à la Java 2 Runtime Environment "( source ).

2
eis