Есть отличное объяснение этой проблемы от Андрея Пангина от 7 апреля 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
call - что изменится?
Здесь мы снова видим взаимоблокировку [ранее в статье было несколько примеров взаимоблокировок в инициализаторах классов]. Поскольку parallel()
потоковые операции выполняются в отдельном пуле потоков. Эти потоки пытаются выполнить тело лямбда, которое записано в байт-коде как private static
метод внутри StreamSum
класса. Но этот метод не может быть выполнен до завершения статического инициализатора класса, который ожидает результатов завершения потока.
Что еще более поразительно: этот код работает по-разному в разных средах. Он будет правильно работать на машине с одним процессором и, скорее всего, будет зависать на машине с несколькими процессорами. Это различие связано с реализацией пула Fork-Join. Вы можете убедиться в этом сами, изменив параметр-Djava.util.concurrent.ForkJoinPool.common.parallelism=N