Ответ 1
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
байт-кодом invokestatic
, он вызывает среду выполнения JVM для разрешения ссылки на метод. В рамках этого процесса JVM инициализирует класс, если это необходимо. После успешного разрешения разрешенный метод сохраняется в записи Constant Pool Cache. 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.