Как диагностировать или обнаруживать взаимоблокировки в статических инициализаторах Java

(Является ли использование статических инициализаторов в Java хорошей идеей, выходит за рамки этого вопроса.)

Я встречаюсь с тупиками в моем приложении Scala, которое, как мне кажется, вызвано блокировкой статических инициализаторов в скомпилированных классах.

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

Вот простой пример приложения Java, который блокирует статический инициализатор:

public class StaticDeadlockExample implements Runnable
{
    static
    {
        Thread thread = new Thread(
                new StaticDeadlockExample(),
                "StaticDeadlockExample child thread");
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

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

    public static void sayHello()
    {
        System.out.println("hello from thread " + Thread.currentThread().getName());
    }

    @Override
    public void run() {
        StaticDeadlockExample.sayHello();
    }
}

Если вы запустите это приложение, оно будет заблокировано. Трассировка стека во время тупиковой ситуации (от jstack) содержит следующие два тупиковых потока:

"StaticDeadlockExample child thread" prio=6 tid=0x000000006c86a000 nid=0x4f54 in Object.wait() [0x000000006d38f000]
   java.lang.Thread.State: RUNNABLE
    at StaticDeadlockExample.run(StaticDeadlockExample.java:37)
    at java.lang.Thread.run(Thread.java:619)

   Locked ownable synchronizers:
    - None

"main" prio=6 tid=0x00000000005db000 nid=0x2fbc in Object.wait() [0x000000000254e000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x000000004a6a7870> (a java.lang.Thread)
    at java.lang.Thread.join(Thread.java:1143)
    - locked <0x000000004a6a7870> (a java.lang.Thread)
    at java.lang.Thread.join(Thread.java:1196)
    at StaticDeadlockExample.<clinit>(StaticDeadlockExample.java:17)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:169)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:116)

   Locked ownable synchronizers:
    - None

Мои вопросы следующие

  • Почему первый поток отмечен как RUNNABLE, когда он действительно ждет блокировки? Могу ли я каким-то образом определить "реальное" состояние этого потока?
  • Почему ни один поток не отмечен как владелец каких-либо (релевантных) блокировок, когда на самом деле один хранит статический блокиратор intializer, а другой ожидает его? Могу ли я каким-то образом обнаружить блокировку блокировки инициализатора?

Ответ 1

Scala позволяет легко попасть в ловушку.

Легкое обходное решение или диагностика (если вы видите клинику в трассировке стека) - это позволить вашему объекту расширить приложение, чтобы DelayedInit удалил ваш код из статического инициализатора.

Некоторые поясняющие ссылки:

https://issues.scala-lang.org/browse/SI-7646

Scala: Параллельная коллекция в инициализаторе объектов заставляет программу зависать

http://permalink.gmane.org/gmane.comp.lang.scala.user/72499

Ответ 2

Я пробовал этот пример с мой инструмент, и он также не смог обнаружить это как тупик. После нескольких попыток с отладчиком jconsole и повторного запуска примера пару раз я заметил, что начальный поток отмечен как RUNNABLE, потому что он запущен, проблема в том, что с момента запуска потока доступа к статическому члену эта операция помещается в очередь после завершения статического блока инициализатора (эта семантика не ясна в спецификации JVM, однако, похоже, это так).

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