Почему параллельный поток с лямбда в статическом инициализаторе вызывает взаимоблокировку?
я столкнулся со странной ситуацией, когда использование параллельного потока с лямбдой в статическом инициализаторе занимает, казалось бы, вечность без использования процессора. Вот код:
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-внутренний.
3 ответов
Я нашел сообщение об ошибке очень похожего случая (JDK-8143380), который был закрыт как" не проблема " Стюартом Марксом:
Это взаимоблокировка инициализации класса. Основной поток тестовой программы выполняет статический инициализатор класса, который задает флаг незавершенной инициализации для класса; этот флаг остается установленным до завершения статического инициализатора. Статический инициализатор выполняет параллельный поток, который вызывает вычисление лямбда-выражений в других нитях. Эти потоки блокируют ожидание завершения инициализации класса. Однако основной поток блокируется в ожидании завершения параллельных задач, что приводит к взаимоблокировке.
тестовая программа должна быть изменена для перемещения логики параллельного потока за пределы статического инициализатора класса. Закрытие как не проблема.
Я смог найти еще один отчет об ошибке этого (JDK-8136753), также закрыт как "не проблема" Стюарт Маркс:
Это тупик, который происходит, потому что статический инициализатор фруктового перечисления плохо взаимодействует с инициализацией класса.
см. спецификацию языка Java, раздел 12.4.2 для получения подробной информации об инициализации класса.
http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2
вкратце, что происходит следующим образом.
- главная поток ссылается на класс Fruit и запускает процесс инициализации. Это устанавливает флаг незавершенной инициализации и запускает статический инициализатор в основном потоке.
- статический инициализатор запускает некоторый код в другом потоке и ждет его завершения. В этом примере используются параллельные потоки, но это не имеет ничего общего с потоками как таковыми. Выполнение кода в другом потоке любым способом и ожидание завершения этого кода будет иметь тот же эффект.
- код в другом потоке ссылается на класс Fruit, который проверяет флаг незавершенной инициализации. Это заставляет другой поток блокировать, пока флаг не будет очищен. (См. Шаг 2 JLS 12.4.2.)
- основной поток блокируется в ожидании завершения другого потока, поэтому статический инициализатор никогда не завершается. Поскольку флаг незавершенной инициализации не очищается до завершения статического инициализатора, потоки блокируются.
чтобы избежать этого проблема. убедитесь, что статическая инициализация класса завершается быстро, не заставляя другие потоки выполнять код, который требует, чтобы этот класс завершил инициализацию.
закрытие как не проблема.
отметим, что FindBugs имеет открытую проблему для добавления предупреждения для этой ситуации.
для тех, кому интересно, где находятся другие потоки, ссылающиеся на Deadlock
сам класс, Java lambdas ведут себя так, как вы написали это:
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) {}
}
есть отличное объяснение этой проблемы Андрей Пангин, датированный 07 Apr 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
класса. Но этот метод не может быть выполнен до завершения статического инициализатора класса, который ожидает результатов завершения потока.
что больше mindblowing: этот код работает по-разному в различных сред. Он будет работать правильно на одной машине CPU и, скорее всего, будет висеть на многопроцессорной машине. Это отличие происходит от реализации пула Fork-Join. Вы можете проверить это самостоятельно изменив параметр -Djava.util.concurrent.ForkJoinPool.common.parallelism=N