Я столкнулся со странной ситуацией, когда использование параллельного потока с лямбдой в статическом инициализаторе, казалось бы, занимает вечность без использования ЦП. Вот код:
class Deadlock {
static {
IntStream.range(0, 10000).parallel().map(i -> i).count();
System.out.println("done");
}
public static void main(final String[] args) {}
}
Это, по-видимому, минимальный тестовый пример воспроизведения для такого поведения. Если я:
- поместите блок в основной метод вместо статического инициализатора,
- удалить распараллеливание или
- убрать лямбду,
код мгновенно завершается. Кто-нибудь может объяснить такое поведение? Это ошибка или это задумано?
Я использую OpenJDK версии 1.8.0_66-internal.
i -> i
не является ссылкой на метод, онstatic method
реализован в классе Deadlock. Если заменитьi -> i
сFunction.identity()
этим кодом должно быть хорошо.Ответы:
Я нашел отчет об ошибке очень похожего дела ( JDK-8143380 ), который Стюарт Маркс закрыл как «Не проблема»:
Мне удалось найти еще один отчет об этой ошибке ( JDK-8136753 ), также закрытый Стюартом Марксом как «Не проблема»:
Обратите внимание, что у FindBugs есть открытая проблема с добавлением предупреждения для этой ситуации.
источник
this
уйти во время создания объекта. Основное правило: не используйте в инициализаторах многопоточные операции. Не думаю, что это сложно понять. Ваш пример регистрации реализованной лямбда-функции в реестре - это другое дело, он не создает взаимоблокировок, если вы не собираетесь ждать одного из этих заблокированных фоновых потоков. Тем не менее, я настоятельно не рекомендую выполнять такие операции в инициализаторе класса. Это не то, для чего они предназначены.Для тех, кому интересно, где другие потоки, ссылающиеся на
Deadlock
сам класс, лямбда-выражения Java ведут себя так, как вы написали: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) {} }
С обычными анонимными классами тупика нет:
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) {} }
источник
lambda1
этому примеру). Помещение каждой лямбды в отдельный класс было бы значительно дороже.i -> i
; они не будут нормой. Лямбда-выражения могут использовать все члены окружающего их класса, включаяprivate
их, и это делает сам определяющий класс их естественным местом. Допустить, чтобы все эти варианты использования страдали от реализации, оптимизированной для особого случая инициализаторов классов с многопоточным использованием тривиальных лямбда-выражений, без использования членов их определяющего класса, не является жизнеспособным вариантом.Есть отличное объяснение этой проблемы от Андрея Пангина от 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
источник