Natknąłem się na dziwną sytuację, w której użycie równoległego strumienia z lambdą w statycznym inicjatorze zajmuje pozornie wieczność bez użycia procesora. Oto kod:
class Deadlock {
static {
IntStream.range(0, 10000).parallel().map(i -> i).count();
System.out.println("done");
}
public static void main(final String[] args) {}
}
Wydaje się, że jest to minimalne odtworzenie przypadku testowego dla tego zachowania. Jeśli ja:
- umieść blok w głównej metodzie zamiast statycznego inicjatora,
- usuń równoległość lub
- usunąć lambdę,
kod natychmiast się kończy. Czy ktoś może wyjaśnić to zachowanie? Czy to błąd, czy jest to zamierzone?
Używam OpenJDK w wersji 1.8.0_66-internal.
i -> i
nie jest odwołaniem do metody, jeststatic method
zaimplementowany w klasie zakleszczenia. Jeśli zastąpići -> i
zFunction.identity()
tym kodem powinno być dobrze.Odpowiedzi:
Znalazłem raport o błędzie dotyczący bardzo podobnej sprawy ( JDK-8143380 ), która została zamknięta jako „Not an Issue” przez Stuarta Marksa:
Udało mi się znaleźć inny raport o błędzie ( JDK-8136753 ), również zamknięty jako „Not an Issue” autorstwa Stuarta Marksa:
Zauważ, że FindBugs ma otwarty problem z dodaniem ostrzeżenia o tej sytuacji.
źródło
this
ucieczkę podczas budowy obiektu. Podstawowa zasada brzmi: nie używaj operacji wielowątkowych w inicjatorach. Nie sądzę, żeby było to trudne do zrozumienia. Twój przykład rejestracji funkcji zaimplementowanej przez lambdę w rejestrze to inna sprawa, nie tworzy zakleszczeń, chyba że masz zamiar czekać na jeden z tych zablokowanych wątków w tle. Niemniej jednak zdecydowanie odradzam wykonywanie takich operacji w inicjatorze klas. Nie do tego są przeznaczone.Dla tych, którzy zastanawiają się, gdzie są inne wątki odwołujące się do
Deadlock
samej klasy, lambdy Java zachowują się tak, jak napisałeś:public class Deadlock { public static int lambda1(int i) { return i; } static { IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() { @Override public int applyAsInt(int operand) { return lambda1(operand); } }).count(); System.out.println("done"); } public static void main(final String[] args) {} }
Przy zwykłych klasach anonimowych nie ma impasu:
public class Deadlock { static { IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() { @Override public int applyAsInt(int operand) { return operand; } }).count(); System.out.println("done"); } public static void main(final String[] args) {} }
źródło
lambda1
i w tym przykładzie). Umieszczenie każdej lambdy w jej własnej klasie byłoby znacznie droższe.i -> i
; nie będą normą. Wyrażenia lambda mogą używać wszystkich członków swojej otaczającej ich klasy, w tym klas,private
co sprawia, że sama klasa definiująca jest ich naturalnym miejscem. Pozwolenie, aby wszystkie te przypadki użycia cierpiały z powodu implementacji zoptymalizowanej dla specjalnego przypadku inicjatorów klas z wielowątkowym użyciem trywialnych wyrażeń lambda, bez używania elementów składowych ich definiującej klasy, nie jest realną opcją.Jest doskonałe wyjaśnienie tego problemu przez Andrieja Pangina , datowane na 7 kwietnia 2015 r. Jest dostępne tutaj , ale jest napisane w języku rosyjskim (i tak proponuję przejrzeć próbki kodu - są one międzynarodowe). Ogólnym problemem jest blokada podczas inicjalizacji klasy.
Oto kilka cytatów z artykułu:
Według JLS każda klasa ma unikalną blokadę inicjalizacji, która jest przechwytywana podczas inicjalizacji. Gdy inny wątek spróbuje uzyskać dostęp do tej klasy podczas inicjalizacji, zostanie zablokowany na zamku do czasu zakończenia inicjalizacji. Gdy klasy są inicjowane jednocześnie, można uzyskać zakleszczenie.
Napisałem prosty program, który oblicza sumę liczb całkowitych, co powinien wydrukować?
public class StreamSum { static final int SUM = IntStream.range(0, 100).parallel().reduce((n, m) -> n + m).getAsInt(); public static void main(String[] args) { System.out.println(SUM); } }
Teraz usuń
parallel()
lub zastąp lambdęInteger::sum
wywołaniem - co się zmieni?Tutaj znów widzimy zakleszczenie [w artykule było kilka przykładów zakleszczeń w inicjatorach klas]. Ponieważ
parallel()
operacje na strumieniu są uruchamiane w oddzielnej puli wątków. Te wątki próbują wykonać treść lambda, która jest zapisywana w kodzie bajtowym jakoprivate static
metoda wewnątrzStreamSum
klasy. Ale ta metoda nie może zostać wykonana przed zakończeniem statycznego inicjatora klasy, który oczekuje na wyniki zakończenia strumienia.Co więcej, ten kod działa inaczej w różnych środowiskach. Będzie działać poprawnie na komputerze z jednym procesorem i najprawdopodobniej zawiesi się na komputerze z wieloma procesorami. Ta różnica wynika z implementacji puli Fork-Join. Możesz to zweryfikować samodzielnie zmieniając parametr
-Djava.util.concurrent.ForkJoinPool.common.parallelism=N
źródło