2 Punkte von GN⁺ 2025-03-01 | 1 Kommentare | Auf WhatsApp teilen
  • In der Vergangenheit erreichte die CPU-Auslastung meines Systems 3.200 %, sodass alle 32 Kerne vollständig ausgelastet waren
  • Es lief die Java-17-Runtime, und nachdem ich in einem Thread-Dump die CPU-Zeit geprüft und nach CPU-Zeit sortiert hatte, fand ich viele ähnliche Threads
  • Analyse des problematischen Codes
    • Über den Stack-Trace wurde Zeile 29 der Klasse BusinessLogic identifiziert
    • Der betreffende Code iterierte über die Liste unrelatedObjects und fügte dabei den Wert von relatedObject in treeMap ein
    • Das war ineffizienter Code, da innerhalb der Schleife unrelatedObject nicht verwendet wurde

Code-Änderung und Test

  • Die unnötige Schleife wurde entfernt und durch die eine Zeile treeMap.put(relatedObject.a(), relatedObject.b()); ersetzt
  • Vor und nach der Änderung wurden Unit-Tests ausgeführt, das Problem ließ sich jedoch nicht reproduzieren
  • Selbst wenn treeMap und unrelatedObjects jeweils mehr als 1.000.000 Elemente enthielten, trat das Problem nicht auf

Ursache des Problems gefunden

  • Auf treeMap wurde gleichzeitig von mehreren Threads zugegriffen, ohne Synchronisierung
  • Das Problem entstand also dadurch, dass mehrere Threads TreeMap gleichzeitig veränderten

Reproduktion des Problems durch Experimente

  • Es wurde ein Experiment durchgeführt, bei dem mehrere Threads eine gemeinsam genutzte TreeMap zufällig aktualisierten
  • Mit einem try-catch-Block wurde festgelegt, NullPointerException zu ignorieren
  • Das Experiment zeigte, dass die CPU-Auslastung auf bis zu 500 % ansteigen konnte

Fazit

  • Gleichzeitige Änderungen an einer nicht synchronisierten TreeMap können schwerwiegende Performance-Probleme verursachen
  • Um solche Probleme zu vermeiden, wird empfohlen, TreeMap zu synchronisieren oder eine threadsichere Collection wie ConcurrentMap zu verwenden

1 Kommentare

 
GN⁺ 2025-03-01
Hacker-News-Kommentare
  • Ich dachte bisher, Race Conditions würden Datenkorruption oder Deadlocks verursachen, hatte aber nicht bedacht, dass sie auch Performance-Probleme auslösen können. Daten können so beschädigt werden, dass sie Endlosschleifen erzeugen

    • Ich finde, Fehler, anormales Verhalten und Warnungen in einem Projekt sollten grundsätzlich behoben werden. Sie können nämlich scheinbar nicht zusammenhängende Probleme verursachen
    • Dass die zentralen Collections von Java per Design nicht thread-safe sind, ist allgemein bekannt. OP sollte prüfen, ob auch an anderen Stellen im Code mehrere Threads die Collections manipulieren
    • Die einfachste Lösung ist, die TreeMap mit Collections.synchronizedMap zu umhüllen oder auf ConcurrentHashMap umzusteigen und bei Bedarf zu sortieren
    • Einzelne Map-Operationen kann man thread-safe machen, aber ob aufeinanderfolgende Operationen thread-safe sind, ist nicht sicher. Es ist auch nicht sicher, ob das Objekt, dem die TreeMap gehört, thread-safe ist
    • Als umstrittene Lösung besuchte Knoten zu verfolgen, ist kein guter Ansatz. Die Collection bleibt weiterhin nicht thread-safe und kann auf andere subtile Weise fehlschlagen
    • Ein Entwickler mit Blick fürs Detail könnte die Kombination aus Threads und TreeMap bemerken oder vorschlagen, keine TreeMap zu verwenden, wenn keine sortierten Elemente benötigt werden. Hier war das jedoch nicht der Fall
    • Das Problem ist, dass der Vertrag der Collection verletzt wurde; selbst wenn man TreeMap durch HashMap ersetzt, bleibt es falsch
  • In Code mit mehreren laufenden Threads ist die einzige sichere Strategie, alle Objekte unveränderlich zu machen und die Objekte, die nicht unveränderlich sein können, auf kleine, in sich geschlossene und streng kontrollierte Bereiche zu beschränken

    • Nach diesen Prinzipien wurde das Kernmodul neu geschrieben, und es verwandelte sich von einer ständigen Problemquelle in einen der robustesten Abschnitte der Codebasis
    • Mit diesen Leitlinien wurde auch das Code-Review deutlich einfacher
  • Die Bemerkung „man konnte sich per ssh fast nicht einloggen“ erinnerte mich an meine Zeit im Graduiertenstudium mit einer Sun UltraSparc 170

    • Ein neuer Nutzer oder Student wollte Aufgaben parallel ausführen, teilte eine große Textdatei anhand von Zeilennummern in mehrere Abschnitte und verarbeitete jeden Abschnitt parallel
    • Es wurde viel RAM verbraucht, und Swap-Versuche führten zu heftigem Hin- und Hersuchen, um verschiedene Abschnitte derselben Datei zu lesen
    • An der Konsole bekam man keinen Login-Prompt, aber es gab bereits eine eingeloggte Sitzung, und darüber konnte man eine Root-Session bekommen und das Problem beheben
    • Das Problem war, dass die Grenzen des Systems nicht verstanden wurden
  • Der Code lässt sich einfach auf Folgendes reduzieren

    • Der ursprüngliche Code führte treeMap.put nur aus, wenn <i>unrelatedObjects</i> nicht leer war. Das könnte bereits ein Bug sein
    • Man sollte prüfen, ob <i>a</i> und <i>b</i> jedes Mal dieselben Werte zurückgeben und ob sich <i>treeMap</i> wie eine Map verhält
  • Eine weitere Möglichkeit, eine Endlosschleife zu erzeugen, ist die Verwendung einer <i>Comparator</i>- oder <i>Comparable</i>-Implementierung, die keine konsistente totale Ordnung implementiert

    • Das hat nichts mit Nebenläufigkeit zu tun und kann je nach konkreten Daten und Verarbeitungsreihenfolge auftreten
  • Man könnte erwägen, Zyklen mit einem hochzählenden Counter zu erkennen und eine Exception zu werfen, wenn die Baumtiefe oder die Größe der Collection überschritten wird

    • Das verursacht fast keinen zusätzlichen Speicher- oder CPU-Overhead und dürfte eher akzeptiert werden
  • In Java erzeugen konkurrierende Operationen auf nicht thread-safe Objekten die interessantesten Bugs

  • Es gibt die Frage, ob eine ungeschützte TreeMap 3.200 % Auslastung verursachen kann

    • Ich habe um 2009 ein ähnliches Problem gesehen, und es kann offenbar immer noch auftreten
    • Für Leute, die Daten-Races für nur ein bisschen problematisch halten, ist das ernüchternd
  • Der Autor hat eine Art Poison Pill entdeckt. Das ist in Event-Sourcing-Systemen verbreiteter: eine Nachricht, die alles umbringt, womit sie in Kontakt kommt

    • Sobald eine Datenstruktur einen ungültigen Zustand erreicht, bleiben alle nachfolgenden Threads in derselben logischen Zeitbombe hängen
  • Exceptions in Threads sind ein absolutes Problem

    • Es gibt Geschichten von Horror-Bugjagden mit C++, select() und Threads, die mit Exceptions um sich werfen