Є чудове пояснення цієї проблеми від Андрія Пангіна від 07 квітня 2015 р. Вона доступна тут , але написана російською мовою (я все-таки пропоную переглянути зразки коду - вони міжнародні). Загальна проблема - блокування під час ініціалізації класу.
Ось кілька цитат із статті:
Згідно з JLS , кожен клас має унікальний замок ініціалізації, який фіксується під час ініціалізації. Коли інший потік намагається отримати доступ до цього класу під час ініціалізації, він буде заблокований на блокуванні до завершення ініціалізації. Коли паралельно ініціалізуються класи, можна отримати глухий кут.
Я написав просту програму, яка обчислює суму цілих чисел, що вона повинна надрукувати?
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);
}
}
Тепер видаліть parallel()
або замініть лямбду на Integer::sum
дзвінок - що зміниться?
Тут ми знову бачимо глухий кут [раніше в статті було кілька прикладів тупикових ситуацій в ініціалізаторах класів]. Через parallel()
потокові операції виконуються в окремому пулі потоків. Ці потоки намагаються виконати лямбда-тіло, яке записано в байт-коді як private static
метод всередині StreamSum
класу. Але цей метод не може бути виконаний до завершення статичного ініціалізатора класу, який чекає результатів завершення потоку.
Що ще більше вражає: цей код працює по-різному в різних середовищах. Він буде правильно працювати на одній машині з процесором і, швидше за все, буде зависати на машині з декількома процесорами. Ця відмінність походить від реалізації пулу Fork-Join. Ви можете перевірити це самостійно, змінивши параметр-Djava.util.concurrent.ForkJoinPool.common.parallelism=N