Wie sollte ich meinen Code reparieren, da HashMaps in jdk1.6 und höher Probleme mit Multi = Threading verursachen?


83

Ich habe kürzlich eine Frage in stackoverflow gestellt und dann die Antwort gefunden. Die erste Frage war: Welche anderen Mechanismen als Mutexe oder Garbage Collection können mein Java-Programm mit mehreren Threads verlangsamen?

Zu meinem Entsetzen stellte ich fest, dass HashMap zwischen JDK1.6 und JDK1.7 geändert wurde. Es hat jetzt einen Codeblock, der bewirkt, dass alle Threads, die HashMaps erstellen, synchronisiert werden.

Die Codezeile in JDK1.7.0_10 lautet

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

Was am Ende anruft

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

Wenn ich in anderen JDKs nachschaue, finde ich, dass dies in JDK1.5.0_22 oder JDK1.6.0_26 nicht vorhanden ist.

Die Auswirkungen auf meinen Code sind enorm. Es macht es so, dass wenn ich auf 64 Threads laufe, ich weniger Leistung bekomme als wenn ich auf 1 Thread laufe. Ein JStack zeigt, dass die meisten Threads die meiste Zeit damit verbringen, sich in dieser Schleife in Random zu drehen.

Ich habe also einige Möglichkeiten:

  • Schreiben Sie meinen Code neu, damit ich keine HashMap verwende, sondern etwas Ähnliches
  • Spielen Sie irgendwie mit dem rt.jar herum und ersetzen Sie die darin enthaltene Hashmap
  • Verwirren Sie sich irgendwie mit dem Klassenpfad, sodass jeder Thread seine eigene Version von HashMap erhält

Bevor ich einen dieser Pfade beschreite (alle sehen sehr zeitaufwändig und potenziell wirkungsvoll aus), habe ich mich gefragt, ob ich einen offensichtlichen Trick verpasst habe. Kann jemand von euch Überlauf-Leute vorschlagen, welcher der bessere Weg ist, oder vielleicht eine neue Idee identifizieren?

Danke für die Hilfe


2
Was erfordert, dass Sie so viele Hashmaps erstellen? Was versuchst du zu machen?
fge

3
2 Kommentare: 1. ConcurrentHashMap scheint das nicht zu verwenden - könnte es eine Alternative sein? 2. Dieser Code wird nur bei der Kartenerstellung aufgerufen. Das bedeutet, dass Sie Millionen von Hashmaps unter starker Konkurrenz erstellen. Spiegelt dies wirklich eine realistische Produktionslast wider?
Assylias

1
Tatsächlich verwendet ConcurrentHashMap diese Methode auch (in Oracle JDK 1.7_10) - anscheinend jedoch nicht OpenJDK 7 .
Assylias

1
@assylias Hier solltest du die neueste Version überprüfen . Dieser hat eine solche Codezeile.
Marko Topolnik

3
@StaveEscura AtomicLongsetzt auf geringe Schreibkonflikte, um gut zu funktionieren. Sie haben hohe Schreibkonflikte, daher benötigen Sie regelmäßige exklusive Sperren. Wenn Sie eine synchronisierte HashMapFactory schreiben, werden Sie wahrscheinlich eine Verbesserung feststellen, es sei denn, Sie tun in diesen Threads nur eine Karteninstanziierung.
Marko Topolnik

Antworten:


56

Ich bin der ursprüngliche Autor des Patches, der in 7u6, CR # 7118743: Alternatives Hashing für String mit Hash-basierten Maps‌, veröffentlicht wurde.

Ich werde gleich zu Beginn anerkennen, dass die Initialisierung von hashSeed ein Engpass ist, aber wir haben nicht erwartet, dass dies ein Problem ist, da es nur einmal pro Hash Map-Instanz auftritt. Damit dieser Code ein Engpass ist, müssen Sie Hunderte oder Tausende von Hash-Maps pro Sekunde erstellen. Das ist sicherlich nicht typisch. Gibt es wirklich einen triftigen Grund für Ihre Bewerbung, dies zu tun? Wie lange leben diese Hash-Maps?

Unabhängig davon werden wir wahrscheinlich die Umstellung auf ThreadLocalRandom anstelle von Random und möglicherweise eine Variante der verzögerten Initialisierung untersuchen, wie von cambecc vorgeschlagen.

BEARBEITEN 3

Ein Fix für den Engpass wurde in das Quecksilber-Repo des JDK7-Updates verschoben:

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

Das Update wird Teil der kommenden 7u40-Version sein und ist bereits in IcedTea 2.4-Versionen verfügbar.

Nahezu endgültige Testversionen von 7u40 sind hier verfügbar:

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

Feedback ist weiterhin willkommen. Senden Sie es an http://mail.openjdk.java.net/mailman/listinfo/core-libs-dev , um sicherzustellen, dass es von den openJDK-Entwicklern gesehen wird.


1
Vielen Dank, dass Sie sich damit befasst haben. Ja, es ist wirklich notwendig, so viele Karten zu erstellen: Die Anwendung ist eigentlich recht einfach, aber 100.000 Menschen können sie pro Sekunde treffen, und das bedeutet, dass Millionen von Karten sehr schnell erstellt werden können. Ich kann es natürlich umschreiben, um keine Karten zu verwenden, aber das ist mit sehr hohen Entwicklungskosten verbunden. Im Moment sieht der Plan, das Zufallsfeld mit Reflexion zu hacken, gut aus
Stave Escura

2
Mike, ein Vorschlag für eine kurzfristige Lösung: Abgesehen von ThreadLocalRandom (das seine eigenen Probleme mit Anwendungen hat, die mit dem threadlokalen Speicher in Konflikt geraten) wäre es nicht viel einfacher und billiger (in Bezug auf Zeit, Risiko und Tests) Hashing.Holder.SEED_MAKER in ein Array von (sagen wir) <num cores> zufälligen Instanzen streifen und die ID des aufrufenden Threads verwenden, um% -index hineinzugeben? Dies sollte Konflikte pro Thread sofort lindern (aber nicht beseitigen), ohne dass merkliche Nebenwirkungen auftreten.
Holger Hoffstätte

10
@ mduigou Webanwendungen mit einer hohen Anforderungsrate und JSON erstellen eine große Anzahl von HashMaps pro Sekunde, da die meisten, wenn nicht alle JSON-Bibliotheken HashMaps oder LinkedHashMaps zum Deserialisieren von JSON-Objekten verwenden. Webanwendungen, die JSON verwenden, sind weit verbreitet, und die Erstellung von HashMaps wird möglicherweise nicht von der Anwendung (sondern von einer Bibliotheksanwendung) gesteuert. Daher würde ich sagen, dass es triftige Gründe gibt, beim Erstellen von HashMaps keinen Engpass zu haben.
sbordet

3
@mduigou Vielleicht besteht eine einfache Erleichterung darin, einfach zu überprüfen, ob der oldSeed derselbe ist, bevor Sie den CAS darauf aufrufen. Diese Optimierung (bekannt als Test-Test und Set oder TTAS) mag redundant erscheinen, kann jedoch wichtige Auswirkungen auf die Leistung haben, da das CAS nicht versucht wird, wenn es bereits weiß, dass es fehlschlagen wird. Fehlgeschlagenes CAS hat den unglücklichen Nebeneffekt, dass der MESI-Status der Cache-Zeile auf Ungültig gesetzt wird. Alle Parteien müssen den Wert erneut aus dem Speicher abrufen. Natürlich ist Holgers Streifenbildung der Samen eine ausgezeichnete langfristige Lösung, aber selbst dann sollte die TTAS-Optimierung verwendet werden.
Jed Wesley-Smith

5
Meinen Sie "Hunderttausende" statt "Hunderttausende"? - GROSSER Unterschied
Michael Neale

30

Dies sieht aus wie ein "Fehler", den Sie umgehen können. Es gibt eine Eigenschaft, die die neue Funktion "Alternatives Hashing" deaktiviert:

jdk.map.althashing.threshold = -1

Das Deaktivieren von alternativem Hashing ist jedoch nicht ausreichend, da dadurch die Erzeugung eines zufälligen Hash-Seeds nicht deaktiviert wird (obwohl dies eigentlich der Fall sein sollte). Selbst wenn Sie das Alt-Hashing deaktivieren, treten während der Instanziierung der Hash-Map immer noch Thread-Konflikte auf.

Eine besonders unangenehme Möglichkeit, dies zu umgehen, besteht darin, die Randomfür die Hash-Seed-Generierung verwendete Instanz durch Ihre eigene nicht synchronisierte Version zu ersetzen :

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

Warum ist es (wahrscheinlich) sicher, dies zu tun? Da das Alt-Hashing deaktiviert wurde, werden die zufälligen Hash-Seeds ignoriert. Es spielt also keine Rolle, dass unsere Instanz von Randomtatsächlich nicht zufällig ist. Wie immer bei solchen bösen Hacks, bitte mit Vorsicht verwenden.

(Dank an https://stackoverflow.com/a/3301720/1899721 für den Code, der statische Endfelder festlegt).

--- Bearbeiten ---

FWIW, die folgende Änderung HashMapwürde den Thread-Konflikt beseitigen, wenn Alt-Hashing deaktiviert ist:

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

Ein ähnlicher Ansatz kann für ConcurrentHashMapusw. verwendet werden.


1
Danke dir. Dies ist zwar ein Hack, löst aber das Problem vorübergehend. Es ist sicherlich eine bessere Lösung als jede andere in der Liste, die ich oben identifiziert habe. Langfristig werde ich sowieso etwas mit einer schnelleren HashMap machen müssen. Dies erinnert mich daran, dass die Lösung für den alten ResourceBundle-Cache nicht löschbar ist. Der Code ist fast identisch!
Stab Escura


3

Es gibt viele Apps, die in Big-Data-Anwendungen eine vorübergehende HashMap pro Datensatz erstellen. Dies sind zum Beispiel Parser und Serialisierer. Das Einfügen einer Synchronisation in nicht synchronisierte Sammlungsklassen ist ein echtes Problem. Meiner Meinung nach ist dies nicht akzeptabel und muss so schnell wie möglich behoben werden. Die Änderung, die anscheinend in 7u6, CR # 7118743 eingeführt wurde, sollte zurückgesetzt oder behoben werden, ohne dass eine Synchronisation oder eine atomare Operation erforderlich ist.

Irgendwie erinnert mich das an den kolossalen Fehler, StringBuffer und Vector und HashTable in JDK 1.1 / 1.2 synchronisiert zu haben. Die Leute haben jahrelang teuer für diesen Fehler bezahlt. Diese Erfahrung muss nicht wiederholt werden.


2

Vorausgesetzt, Ihr Nutzungsmuster ist angemessen, möchten Sie Ihre eigene Version von Hashmap verwenden.

Dieser Code dient dazu, Hash-Kollisionen viel schwerer zu verursachen und Angreifer daran zu hindern, Leistungsprobleme ( Details ) zu verursachen. Vorausgesetzt, dieses Problem wird bereits auf andere Weise behandelt, benötigen Sie meiner Meinung nach überhaupt keine Synchronisierung. Unabhängig davon, ob Sie die Synchronisierung verwenden oder nicht, möchten Sie anscheinend Ihre eigene Version von Hashmap verwenden, damit Sie nicht so sehr davon abhängen, was JDK gerade bereitstellt.

Entweder schreiben Sie normalerweise etwas Ähnliches und zeigen darauf oder Sie überschreiben eine Klasse in JDK. Um letzteres zu tun, können Sie den Bootstrap-Klassenpfad mit dem -Xbootclasspath/p:Parameter überschreiben . Dies würde jedoch "gegen die Java 2 Runtime Environment-Binärcodelizenz verstoßen" ( Quelle ).


Aha. Ich hatte nicht bemerkt, dass dies der Punkt der Optimierung war. Sehr schlau. Mein Bedrohungsmodell für Angreifer lässt sie nicht auf diese Weise mit Hashmaps herumspielen, aber ich werde mich für die Zukunft daran erinnern. Ich stimme Ihrem Standpunkt zu, die HashMap eventuell zu ersetzen. Ich werde wahrscheinlich ein Factory-Objekt oder vielleicht einen IOC-Container in jede Klasse einfädeln müssen, die sie erstellt. Ich denke, die Antwort von Cambecc wird mich aus dem Loch bringen, während ich an einer längerfristigen Lösung arbeite
Stave Escura
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.