Почему использование параллельных потоков в статическом инициализаторе приводит к нестабильной тупиковой ситуации - PullRequest
0 голосов
/ 11 декабря 2018

ВНИМАНИЕ: это не дубликат, пожалуйста, прочитайте тему осторожно https://stackoverflow.com/users/3448419/apangin цитата:

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

В комментариях https://stackoverflow.com/a/53709217/2674303 я попытался выяснить причины, по которым код ведет себя по-разному от одного начала к другому и участников этого обсуждениядал мне совет создать отдельную тему.

Давайте рассмотрим следующий исходный код:

public class Test {
    static {
        System.out.println("static initializer: " + Thread.currentThread().getName());

        final long SUM = IntStream.range(0, 5)
                .parallel()
                .mapToObj(i -> {
                    System.out.println("map: " + Thread.currentThread().getName() + " " + i);
                    return i;
                })
                .sum();
    }

    public static void main(String[] args) {
        System.out.println("Finished");
    }
}

Иногда (почти всегда) это приводит к тупику.

Примервыходных данных:

static initializer: main
map: main 2
map: ForkJoinPool.commonPool-worker-3 4
map: ForkJoinPool.commonPool-worker-3 3
map: ForkJoinPool.commonPool-worker-2 0

Но иногда это заканчивается успешно (очень редко):

static initializer: main
map: main 2
map: main 3
map: ForkJoinPool.commonPool-worker-2 4
map: ForkJoinPool.commonPool-worker-1 1
map: ForkJoinPool.commonPool-worker-3 0
Finished

или

static initializer: main
map: main 2
map: ForkJoinPool.commonPool-worker-2 0
map: ForkJoinPool.commonPool-worker-1 1
map: ForkJoinPool.commonPool-worker-3 4
map: main 3

Не могли бы вы объяснить такое поведение?

1 Ответ

0 голосов
/ 19 декабря 2018

TL; DR Это ошибка HotSpot JDK-8215634

Проблема может быть воспроизведена с помощью простого контрольного примера, который имеетвообще никаких гонок:

public class StaticInit {

    static void staticTarget() {
        System.out.println("Called from " + Thread.currentThread().getName());
    }

    static {
        Runnable r = new Runnable() {
            public void run() {
                staticTarget();
            }
        };

        r.run();

        Thread thread2 = new Thread(r, "Thread-2");
        thread2.start();
        try { thread2.join(); } catch (Exception ignore) {}

        System.out.println("Initialization complete");
    }

    public static void main(String[] args) {
    }
}

Это похоже на классический тупик инициализации, но JSM HotSpot не зависает.Вместо этого он печатает:

Called from main
Called from Thread-2
Initialization complete

Почему это ошибка

JVMS §6.5 требует, чтобы при выполнении invokestatic байт-код

класс или интерфейс, который объявил разрешенный метод, инициализируется, если этот класс или интерфейс еще не был инициализирован

Когда Thread-2 вызывает staticTarget, основной класс StaticInit явно неинициализирован (так как его статический инициализатор все еще работает).Это означает, что Thread-2 должен запустить процедуру инициализации класса, описанную в JVMS §5.5 .Согласно этой процедуре

Если объект Class для C указывает, что инициализация для C выполняется другим потоком, то отпустите LC и заблокируйте текущий поток, пока не будет сообщено, что текущая инициализация завершена

Однако Thread-2 не блокируется, несмотря на то, что класс находится в процессе инициализации потоком main.

Как насчет других JVM

Я тестировал OpenJ9 и JET, и они оба ожидаемотупик в приведенном выше тесте.
Интересно, что HotSpot также зависает в режиме -Xcomp, но не в -Xint или смешанных режимах.

Как это происходит

Когда переводчик впервые встречаетсяinvokestatic байт-код, он вызывает среду выполнения JVM для разрешения ссылки на метод.В рамках этого процесса JVM инициализирует класс, если это необходимо.После успешного разрешения разрешенный метод сохраняется в записи Constant Pool Cache.Кэш констант пула - это специфическая для HotSpot структура, в которой хранятся разрешенные постоянные значения пула.

В приведенном выше тесте байт-код invokestatic, который вызывает staticTarget, сначала разрешается потоком main.Среда выполнения интерпретатора пропускает инициализацию класса, потому что класс уже инициализируется тем же потоком.Разрешенный метод сохраняется в кеше постоянного пула.В следующий раз, когда Thread-2 выполнит тот же invokestatic, интерпретатор увидит, что байт-код уже разрешен, и использует постоянную запись в кэше пула без вызова среды выполнения и, таким образом, пропускает инициализацию класса.

Аналогичная ошибка для getstatic / putstatic была исправлена ​​давно - JDK-4493560 , но это исправление не коснулось invokestatic.Я отправил новую ошибку JDK-8215634 для решения этой проблемы.

Что касается исходного примера,

, зависает он или нет, зависит от того, какой поток разрешает первымстатический звонок.Если это поток main, программа завершается без тупика.Если статический вызов разрешен одним из потоков ForkJoinPool, программа зависает.

Обновление

Ошибка подтверждена .Это исправлено в следующих выпусках: JDK 8u201, JDK 11.0.2 и JDK 12.

...