Dies ist eine Art Umfrage zu häufigen Parallelitätsproblemen in Java. Ein Beispiel könnte der klassische Deadlock oder Race-Zustand sein oder vielleicht EDT-Threading-Fehler in Swing. Ich interessiere mich sowohl für eine Vielzahl möglicher Probleme als auch für die am häufigsten auftretenden Probleme. Bitte hinterlassen Sie pro Kommentar eine bestimmte Antwort auf einen Java-Parallelitätsfehler und stimmen Sie ab, wenn Sie eine Antwort sehen, auf die Sie gestoßen sind.
java
multithreading
concurrency
Alex Miller
quelle
quelle
Antworten:
Das häufigste Parallelitätsproblem, das ich gesehen habe, besteht darin, nicht zu erkennen, dass ein von einem Thread geschriebenes Feld nicht garantiert von einem anderen Thread gesehen wird. Eine häufige Anwendung davon:
Solange Anschlag ist nicht flüchtig oder
setStop
undrun
nicht sind synchronisiert diese nicht zur Arbeit gewährleistet ist. Dieser Fehler ist besonders teuflisch, da er in 99,999% in der Praxis keine Rolle spielt, da der Leser-Thread die Änderung irgendwann sehen wird - aber wir wissen nicht, wie schnell er sie gesehen hat.quelle
Mein schmerzhaftestes Parallelitätsproblem Nr. 1 trat jemals auf, als zwei verschiedene Open Source-Bibliotheken so etwas taten:
Auf den ersten Blick sieht dies wie ein ziemlich triviales Synchronisationsbeispiel aus. Jedoch; Da Strings in Java interniert sind ,
"LOCK"
stellt sich heraus, dass der Literal-String dieselbe Instanz von istjava.lang.String
(obwohl sie völlig unterschiedlich voneinander deklariert sind). Das Ergebnis ist offensichtlich schlecht.quelle
Ein klassisches Problem besteht darin, das zu synchronisierende Objekt während der Synchronisierung zu ändern:
Andere gleichzeitige Threads werden dann für ein anderes Objekt synchronisiert, und dieser Block bietet nicht den erwarteten gegenseitigen Ausschluss.
quelle
Ein häufiges Problem ist die Verwendung von Klassen wie Calendar und SimpleDateFormat aus mehreren Threads (häufig durch Zwischenspeichern in einer statischen Variablen) ohne Synchronisierung. Diese Klassen sind nicht threadsicher, sodass Multithread-Zugriff letztendlich seltsame Probleme mit inkonsistentem Status verursacht.
quelle
Double-Checked Locking. Im Großen und Ganzen.
Das Paradigma, mit dem ich bei meiner Arbeit bei BEA angefangen habe, die Probleme zu lernen, ist, dass die Leute einen Singleton folgendermaßen überprüfen:
Dies funktioniert nie, da möglicherweise ein anderer Thread in den synchronisierten Block gelangt ist und s_instance nicht mehr null ist. Die natürliche Veränderung besteht also darin, es zu schaffen:
Das funktioniert auch nicht, weil das Java-Speichermodell es nicht unterstützt. Sie müssen s_instance als flüchtig deklarieren, damit es funktioniert, und selbst dann funktioniert es nur unter Java 5.
Leute, die mit den Feinheiten des Java-Speichermodells nicht vertraut sind, bringen dies ständig durcheinander .
quelle
Nicht ordnungsgemäße Synchronisierung von Objekten, die von zurückgegeben werden
Collections.synchronizedXXX()
, insbesondere während der Iteration oder mehrerer Operationen:Das ist falsch . Trotz einzelner Operationen kann der
synchronized
Status der Zuordnung zwischen dem Aufrufencontains
undput
durch einen anderen Thread geändert werden. Es sollte sein:Oder mit einer
ConcurrentMap
Implementierung:quelle
Obwohl dies wahrscheinlich nicht genau das ist, wonach Sie fragen, ist das häufigste Problem im Zusammenhang mit der Parallelität, auf das ich gestoßen bin (wahrscheinlich, weil es in normalem Single-Thread-Code auftritt) a
java.util.ConcurrentModificationException
verursacht durch Dinge wie:
quelle
Es kann leicht vorstellbar sein, dass synchronisierte Sammlungen Ihnen mehr Schutz bieten als sie tatsächlich, und vergessen, die Sperre zwischen Anrufen zu halten. Ich habe diesen Fehler einige Male gesehen:
In der zweiten Zeile oben sind die Methoden
toArray()
undsize()
beide für sich genommen threadsicher, aber diesize()
werden getrennt von der ausgewertettoArray()
, und die Sperre in der Liste wird zwischen diesen beiden Aufrufen nicht gehalten.Wenn Sie diesen Code mit einem anderen Thread ausführen, der gleichzeitig Elemente aus der Liste entfernt, erhalten Sie früher oder später eine neue
String[]
Rückgabe, die größer als erforderlich ist, um alle Elemente in der Liste aufzunehmen, und im Ende Nullwerte enthält. Es ist leicht zu glauben, dass dies eine atomare Operation ist, da die beiden Methodenaufrufe an die Liste in einer einzigen Codezeile erfolgen, dies jedoch nicht.quelle
Der häufigste Fehler, bei dem ich arbeite, ist, dass Programmierer lange Vorgänge wie Serveraufrufe auf dem EDT ausführen, die GUI für einige Sekunden sperren und die App nicht mehr reagieren lässt.
quelle
Vergessen, in einer Schleife zu warten () (oder Condition.await ()) und zu überprüfen, ob die Wartebedingung tatsächlich erfüllt ist. Ohne dies stoßen Sie auf Fehler von falschen wait () - Weckvorgängen. Kanonische Verwendung sollte sein:
quelle
Ein weiterer häufiger Fehler ist die schlechte Behandlung von Ausnahmen. Wenn ein Hintergrundthread eine Ausnahme auslöst und Sie sie nicht richtig behandeln, wird die Stapelverfolgung möglicherweise überhaupt nicht angezeigt. Oder Ihre Hintergrundaufgabe wird nicht mehr ausgeführt und nie wieder gestartet, weil Sie die Ausnahme nicht behandelt haben.
quelle
Bis ich eine Klasse bei Brian Goetz besucht habe, war mir nicht klar, dass die Nicht-Synchronisation
getter
eines privaten Feldes, das durch eine Synchronisation mutiertsetter
ist, niemals garantiert den aktualisierten Wert zurückgibt. Nur wenn eine Variable sowohl beim Lesen als auch beim Schreiben durch einen synchronisierten Block geschützt ist, erhalten Sie die Garantie für den neuesten Wert der Variablen.quelle
Ich denke, Sie schreiben Single-Threaded-Code, verwenden jedoch veränderbare Statiken (einschließlich Singletons). Offensichtlich werden sie zwischen Threads geteilt. Dies passiert überraschend oft.
quelle
Beliebige Methodenaufrufe sollten nicht innerhalb synchronisierter Blöcke erfolgen.
Dave Ray hat dies in seiner ersten Antwort angesprochen, und tatsächlich bin ich auch auf einen Deadlock gestoßen, der auch mit dem Aufrufen von Methoden für Listener innerhalb einer synchronisierten Methode zu tun hat. Ich denke, die allgemeinere Lektion ist, dass Methodenaufrufe nicht "in die Wildnis" innerhalb eines synchronisierten Blocks erfolgen sollten - Sie haben keine Ahnung, ob der Aufruf lange dauern wird, zu einem Deadlock führt oder was auch immer.
In diesem Fall und normalerweise im Allgemeinen bestand die Lösung darin, den Umfang des synchronisierten Blocks zu reduzieren, um nur einen kritischen privaten Codeabschnitt zu schützen .
Da wir jetzt außerhalb eines synchronisierten Blocks auf die Sammlung von Listenern zugegriffen haben, haben wir sie in eine Copy-on-Write-Sammlung geändert. Oder wir hätten einfach eine defensive Kopie der Sammlung erstellen können. Der Punkt ist, dass es normalerweise Alternativen gibt, um sicher auf eine Sammlung unbekannter Objekte zuzugreifen.
quelle
Der letzte Fehler im Zusammenhang mit der Parallelität, auf den ich gestoßen bin, war ein Objekt, das in seinem Konstruktor einen ExecutorService erstellt hat. Wenn jedoch nicht mehr auf das Objekt verwiesen wurde, wurde der ExecutorService nie heruntergefahren. So über einen Zeitraum von Wochen, Tausende von Threads durchgesickert, was schließlich zum Absturz des Systems führte. (Technisch gesehen stürzte es nicht ab, funktionierte aber nicht mehr richtig, während es weiter lief.)
Technisch gesehen ist dies vermutlich kein Parallelitätsproblem, sondern ein Problem im Zusammenhang mit der Verwendung der Bibliotheken java.util.concurrency.
quelle
Eine unausgeglichene Synchronisation, insbesondere gegen Karten, scheint ein ziemlich häufiges Problem zu sein. Viele Leute glauben, dass das Synchronisieren von Puts auf eine Map (keine ConcurrentMap, sondern eine HashMap) und das Nicht-Synchronisieren von Pets ausreichend ist. Dies kann jedoch zu einer Endlosschleife beim erneuten Hash führen.
Das gleiche Problem (teilweise Synchronisation) kann jedoch überall dort auftreten, wo Sie den Status für Lese- und Schreibvorgänge freigegeben haben.
quelle
Bei Servlets ist ein Parallelitätsproblem aufgetreten, wenn veränderbare Felder vorhanden sind, die von jeder Anforderung festgelegt werden. Es gibt jedoch nur eine Servlet-Instanz für alle Anforderungen, sodass dies in einer Einzelbenutzerumgebung einwandfrei funktioniert hat. Wenn jedoch mehr als ein Benutzer das Servlet angefordert hat, sind unvorhersehbare Ergebnisse aufgetreten.
quelle
Nicht gerade ein Fehler, aber die schlimmste Sünde ist, eine Bibliothek bereitzustellen, die andere verwenden sollen, aber nicht anzugeben, welche Klassen / Methoden threadsicher sind und welche nur von einem einzelnen Thread usw. aufgerufen werden dürfen.
Mehr Menschen sollten die in Goetz 'Buch beschriebenen Parallelitätsanmerkungen (z. B. @ThreadSafe, @GuardedBy usw.) verwenden.
quelle
Mein größtes Problem waren immer Deadlocks, insbesondere durch Hörer, die mit einem gehaltenen Schloss ausgelöst wurden. In diesen Fällen ist es sehr einfach, eine umgekehrte Verriegelung zwischen zwei Threads zu erreichen. In meinem Fall zwischen einer Simulation, die in einem Thread ausgeführt wird, und einer Visualisierung der Simulation, die im UI-Thread ausgeführt wird.
BEARBEITEN: Der zweite Teil wurde verschoben, um die Antwort zu trennen.
quelle
Das Starten eines Threads im Konstruktor einer Klasse ist problematisch. Wenn die Klasse erweitert ist, kann der Thread gestartet werden, bevor der Konstruktor der Unterklasse ausgeführt wird.
quelle
Veränderbare Klassen in gemeinsam genutzten Datenstrukturen
In diesem Fall ist der Code weitaus komplexer als in diesem vereinfachten Beispiel. Das Replizieren, Finden und Beheben des Fehlers ist schwierig. Vielleicht könnte es vermieden werden, wenn wir bestimmte Klassen als unveränderlich und bestimmte Datenstrukturen als nur unveränderliche Objekte enthaltend markieren könnten.
quelle
Die Synchronisierung mit einem Zeichenfolgenliteral oder einer durch ein Zeichenfolgenliteral definierten Konstante ist (möglicherweise) ein Problem, da das Zeichenfolgenliteral interniert ist und von allen anderen Personen in der JVM mit demselben Zeichenfolgenliteral gemeinsam genutzt wird. Ich weiß, dass dieses Problem bei Anwendungsservern und anderen "Container" -Szenarien aufgetreten ist.
Beispiel:
In diesem Fall verwendet jeder, der die Zeichenfolge "foo" zum Sperren verwendet, dieselbe Sperre.
quelle
Ich glaube, dass das Hauptproblem bei Java in Zukunft die (fehlenden) Sichtbarkeitsgarantien für Konstruktoren sein wird. Zum Beispiel, wenn Sie die folgende Klasse erstellen
und dann lesen Sie einfach die MyClass-Eigenschaft a aus einem anderen Thread. MyClass.a kann je nach Implementierung und Stimmung der JavaVM entweder 0 oder 1 sein. Heute sind die Chancen, dass 'a' 1 ist, sehr hoch. Bei zukünftigen NUMA-Maschinen kann dies jedoch anders sein. Viele Menschen sind sich dessen nicht bewusst und glauben, dass sie sich während der Initialisierungsphase nicht um Multithreading kümmern müssen.
quelle
Der dümmste Fehler, den ich häufig mache, ist das Vergessen der Synchronisierung, bevor ich notify () oder wait () für ein Objekt aufrufe.
quelle
Verwenden eines lokalen "neuen Objekts ()" als Mutex.
Das ist nutzlos.
quelle
Ein weiteres häufiges Problem der Parallelität ist die Verwendung von synchronisiertem Code, wenn dieser überhaupt nicht erforderlich ist. Zum Beispiel sehe ich immer noch Programmierer, die
StringBuffer
oder sogarjava.util.Vector
(als lokale Methodenvariablen) verwenden.quelle
Mehrere Objekte, die gesperrt sind, auf die jedoch häufig nacheinander zugegriffen wird. Wir sind auf einige Fälle gestoßen, in denen die Sperren durch unterschiedlichen Code in unterschiedlicher Reihenfolge erhalten werden, was zu einem Deadlock führt.
quelle
Nicht zu erkennen, dass das
this
in einer inneren Klasse nicht dasthis
der äußeren Klasse ist. Normalerweise in einer anonymen inneren Klasse, die implementiertRunnable
. Das Grundproblem ist, dass die Synchronisation Teil von allem istObject
es keine statische Typprüfung s ist. Ich habe dies mindestens zweimal im Usenet gesehen und es erscheint auch in Brian Goetz'z Java Concurrency in Practice.BGGA-Verschlüsse leiden nicht darunter, da es keine
this
für den Verschluss gibt (this
verweist auf die äußere Klasse). Wenn Sie Nicht-this
Objekte als Sperren verwenden, wird dieses und andere Probleme umgangen.quelle
Verwendung eines globalen Objekts wie einer statischen Variablen zum Sperren.
Dies führt aufgrund von Konflikten zu einer sehr schlechten Leistung.
quelle
Honesly? Vor dem Aufkommen von
java.util.concurrent
war das häufigste Problem, auf das ich routinemäßig gestoßen bin, das, was ich als "Thread-Thrashing" bezeichne: Anwendungen, die Threads für die Parallelität verwenden, aber zu viele davon erzeugen und am Ende Thrashing verursachen.quelle