Почему параллельный поток с лямбда в статическом инициализаторе вызывает взаимоблокировку?


я столкнулся со странной ситуацией, когда использование параллельного потока с лямбдой в статическом инициализаторе занимает, казалось бы, вечность без использования ЦП. Вот код:

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 73

3 ответа:

Я нашел сообщение об ошибке очень похожего случая (JDK-8143380), который был закрыт как "не вопрос" Стюарт отмечает:

Это тупик инициализации класса. Основной поток тестовой программы выполняет статический инициализатор класса, который устанавливает флаг незавершенной инициализации для класса; этот флаг остается установленным до завершения статического инициализатора. Статический инициализатор выполняет параллельный поток, который вызывает вычисление лямбда-выражений в других потоках. Эти потоки блокируют ожидание завершения инициализации класса. Однако основной поток блокируется в ожидании завершения параллельных задач, что приводит к взаимоблокировке.

тестовая программа должна быть изменена, чтобы переместить логику параллельного потока за пределы статического инициализатора класса. Закрытие как не проблема.


Я смог найти еще один отчет об ошибке этого (JDK-8136753), также закрыт как "не проблема" Стюарт Маркс:

Это тупик, который происходит из-за того, что статический инициализатор Fruit enum плохо взаимодействует с инициализацией класса.

см. спецификацию языка Java, раздел 12.4.2 для получения подробной информации об инициализации класса.

http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2

вкратце, происходит следующее.

  1. главная поток ссылается на класс Fruit и запускает процесс инициализации. Это устанавливает флаг незавершенной инициализации и запускает статический инициализатор в главном потоке.
  2. статический инициализатор выполняет некоторый код в другом потоке и ждет его завершения. В этом примере используются параллельные потоки, но это не имеет ничего общего с потоками как таковыми. Выполнение кода в другом потоке любым способом и ожидание завершения этого кода будет иметь тот же эффект.
  3. код в другом потоке ссылается на класс Fruit, который проверяет флаг in-progress инициализации. Это приводит к блокировке другого потока, пока флаг не будет снят. (См. Шаг 2 JLS 12.4.2.)
  4. основной поток блокируется в ожидании завершения другого потока, поэтому статический инициализатор никогда не завершается. Поскольку флаг незавершенной инициализации не снимается до завершения статического инициализатора, потоки находятся в тупике.

чтобы избежать этого проблема, убедитесь, что статическая инициализация класса завершается быстро, не заставляя другие потоки выполнять код, который требует, чтобы этот класс завершил инициализацию.

закрытие как не проблема.


отметим, что 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 апреля 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 класса. Но этот метод не может быть выполнен до завершения класса static initializer, который ждет результатов завершения потока.

что еще умопомрачительно: этот код работает по-разному в различных сред. Он будет работать правильно на одном процессоре и, скорее всего, будет висеть на многопроцессорной машине. Это различие происходит от реализации пула Fork-Join. Вы можете проверить это самостоятельно изменив параметр -Djava.util.concurrent.ForkJoinPool.common.parallelism=N