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 jako private static
metoda wewnątrz StreamSum
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