Ich bin auf eine seltsame Situation gestoßen, in der die Verwendung eines parallelen Streams mit einem Lambda in einem statischen Initialisierer scheinbar ohne CPU-Auslastung ewig dauert. Hier ist der Code:
class Deadlock {
static {
IntStream.range(0, 10000).parallel().map(i -> i).count();
System.out.println("done");
}
public static void main(final String[] args) {}
}
Dies scheint ein minimaler Reproduktionstestfall für dieses Verhalten zu sein. Wenn ich:
- Setzen Sie den Block in die Hauptmethode anstelle eines statischen Initialisierers.
- Parallelisierung entfernen oder
- Entfernen Sie das Lambda,
Der Code wird sofort vervollständigt. Kann jemand dieses Verhalten erklären? Ist es ein Fehler oder ist das beabsichtigt?
Ich verwende OpenJDK Version 1.8.0_66-intern.
i -> i
ist keine Methodenreferenz, sondernstatic method
in der Deadlock-Klasse implementiert. Wenn ersetzti -> i
mitFunction.identity()
diesem Code sollte in Ordnung sein.Antworten:
Ich habe einen Fehlerbericht eines sehr ähnlichen Falls ( JDK-8143380 ) gefunden, der von Stuart Marks als "Not a Issue" geschlossen wurde:
Ich konnte einen weiteren Fehlerbericht darüber finden ( JDK-8136753 ), der von Stuart Marks ebenfalls als "Not an Issue" geschlossen wurde:
Beachten Sie, dass FindBugs ein offenes Problem beim Hinzufügen einer Warnung für diese Situation hat.
quelle
this
der während der Objektkonstruktion entkommen kann. Die Grundregel lautet: Verwenden Sie in Initialisierern keine Multithread-Operationen. Ich denke nicht, dass das schwer zu verstehen ist. Ihr Beispiel für die Registrierung einer von Lambda implementierten Funktion in einer Registrierung ist etwas anderes. Es werden keine Deadlocks erstellt, es sei denn, Sie warten auf einen dieser blockierten Hintergrund-Threads. Trotzdem rate ich dringend davon ab, solche Operationen in einem Klasseninitialisierer durchzuführen. Es ist nicht das, wofür sie gedacht sind.Für diejenigen, die sich fragen, wo sich die anderen Threads befinden, die auf die
Deadlock
Klasse selbst verweisen , verhalten sich Java-Lambdas so, wie Sie dies geschrieben haben:Bei regulären anonymen Klassen gibt es keinen Deadlock:
quelle
lambda1
diesem Beispiel). Jedes Lambda in eine eigene Klasse zu bringen, wäre erheblich teurer gewesen.i -> i
ansehen. Sie werden nicht die Norm sein. Lambda-Ausdrücke können alle Mitglieder ihrer umgebenden Klasse verwenden, einschließlich dererprivate
, und das macht die definierende Klasse selbst zu ihrem natürlichen Platz. Es ist keine praktikable Option, all diese Anwendungsfälle unter einer Implementierung leiden zu lassen, die für den Spezialfall von Klasseninitialisierern optimiert ist, bei denen triviale Lambda-Ausdrücke mit mehreren Threads verwendet werden und keine Mitglieder ihrer definierenden Klasse verwendet werden.Es gibt eine ausgezeichnete Erklärung für dieses Problem von Andrei Pangin vom 07. April 2015. Es ist hier verfügbar , aber in russischer Sprache verfasst (ich empfehle, Codebeispiele trotzdem zu überprüfen - sie sind international). Das allgemeine Problem ist eine Sperre während der Klasseninitialisierung.
Hier sind einige Zitate aus dem Artikel:
Laut JLS verfügt jede Klasse über eine eindeutige Initialisierungssperre , die während der Initialisierung erfasst wird. Wenn ein anderer Thread während der Initialisierung versucht, auf diese Klasse zuzugreifen, wird sie für die Sperre blockiert, bis die Initialisierung abgeschlossen ist. Wenn Klassen gleichzeitig initialisiert werden, kann es zu einem Deadlock kommen.
Ich habe ein einfaches Programm geschrieben, das die Summe der ganzen Zahlen berechnet. Was soll es drucken?
Entfernen
parallel()
oder ersetzen Sie nun Lambda durchInteger::sum
Call - was wird sich ändern?Hier sehen wir wieder einen Deadlock [es gab einige Beispiele für Deadlocks in Klasseninitialisierern zuvor im Artikel]. Aufgrund der
parallel()
Stream-Operationen werden in einem separaten Thread-Pool ausgeführt. Diese Threads versuchen, den Lambda-Body auszuführen, der alsprivate static
Methode innerhalb derStreamSum
Klasse in Bytecode geschrieben ist . Diese Methode kann jedoch nicht vor Abschluss des statischen Klasseninitialisierers ausgeführt werden, der auf die Ergebnisse der Stream-Fertigstellung wartet.Was noch umwerfender ist: Dieser Code funktioniert in verschiedenen Umgebungen unterschiedlich. Es funktioniert ordnungsgemäß auf einem einzelnen CPU-Computer und hängt höchstwahrscheinlich auf einem Multi-CPU-Computer. Dieser Unterschied ergibt sich aus der Implementierung des Fork-Join-Pools. Sie können dies selbst überprüfen, indem Sie den Parameter ändern
-Djava.util.concurrent.ForkJoinPool.common.parallelism=N
quelle