Смешанное использование Log4j и commons-logging вызывает "тупик загрузки класса"?

Я думаю, что я нашел ситуацию, когда смешанное использование log4j a) напрямую и b) через commons-logging вызывает какой-то тупик с загрузкой класса. Я не уверен, что такая ситуация вообще возможна (не должна ли JVM обнаружить это?) И что с этим делать.

Проблема

В нашей системе сборки мы в настоящее время выполняем наши модульные тесты последовательно - чтобы сделать сборку быстрее, мы, очевидно, можем изменить это, чтобы параллельно выполнять наши модульные тесты. Однако, если мы это сделаем, некоторые сборки выполняются в тайм-аут выполнения. При анализе дампа потока таких "висячих сборок" мы находимся в разных модулях с различными тестами, которые проводились большую часть времени. Но он всегда сводится к двум нитям, которые пытаются инициализировать Logger: один с Logger.getLogger (напрямую используя log4j), другой - с LogFactory.getLog (с использованием commons-logging).

Мой анализ

Итак, у нас есть один поток (тот, который использует log4j напрямую), ожидающий в этом месте:

"pool-1-thread-3" prio=10 tid=0x00007f6528ca6000 nid=0x6f8f in Object.wait() [0x00007f64d9ca6000]
  java.lang.Thread.State: RUNNABLE
at org.apache.log4j.LogManager.<clinit>(LogManager.java:82)
at org.apache.log4j.Logger.getLogger(Logger.java:117)
at de.is24.platform.contacts.domain.PhoneNumberFormat.<clinit>(PhoneNumberFormat.java:21)

который, к сожалению, является довольно "переполненной" строкой:

Hierarchy h = new Hierarchy(new RootLogger((Level) Level.DEBUG));

И еще один поток (используя commons-logging) ждет здесь:

"pool-1-thread-2" prio=10 tid=0x00007f6528bf9800 nid=0x6f8e in Object.wait() [0x00007f64d9da7000]
  java.lang.Thread.State: RUNNABLE
at org.apache.log4j.Priority.<clinit>(Priority.java:45)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:171)
at org.apache.commons.logging.impl.Log4JLogger.class$(Log4JLogger.java:37)
at org.apache.commons.logging.impl.Log4JLogger.<clinit>(Log4JLogger.java:45)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactoryImpl.java:529)
at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactoryImpl.java:235)
at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactoryImpl.java:209)
at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:351)

что является простым:

final static public Priority FATAL = new Level(FATAL_INT, "FATAL", 0);

Итак, мне кажется, что второй поток находится в процессе инициализации класса Priority и ждет загрузки класса Level. Первый поток, вероятно, пытается загрузить класс Level (другой материал в этой строке кажется несвязанным), а по мере того, как класс Level extends Priority, ожидает, что класс Priority будет загружен.
Там у нас наш тупик.

Итак, можете ли вы сказать мне, правильно ли этот анализ? Или я что-то пропустил?

ОБНОВЛЕНИЕ I

Я написал несколько тестовых примеров, вы можете найти их здесь: https://github.com/sebastiankirsch/classloading

Существует несколько тестовых примеров, демонстрирующих проблему:

  • TestLoadingByClassForName должен вызвать тупик довольно быстро (каждый третий запуск на моей машине)
  • TestLoadingMixed представляет собой версию проблемы, урезанной до минимума наблюдаемой трассировки стека; это происходит гораздо нечасто (примерно в 4 раз).
  • TestMixedLoggerInstantiation имитирует поведение: один класс создает экземпляр регистратора с использованием log4j, другой - с использованием commons-logging. Тупик трудно поймать здесь, так как есть гораздо больше кода - мне нужно добавить случайный сон, который, безусловно, необходимо адаптировать к вашей машине.

Здесь трассировка стека тестового примера TestMixedLoggerInstantiation.

Full thread dump Java HotSpot(TM) 64-Bit Server VM (20.1-b02 mixed mode):

"UseLog4JLogger" prio=10 tid=0x00007fa1f017d800 nid=0x7bd8 in Object.wait() [0x00007fa1e5ba4000]
   java.lang.Thread.State: RUNNABLE
    at org.apache.log4j.LogManager.<clinit>(LogManager.java:82)
    at org.apache.log4j.Logger.getLogger(Logger.java:117)
    at net.tcc.classloading.UseLog4JLogger.run(UseLog4JLogger.java:23)

"UseCommonsLoggingLogFactory" prio=10 tid=0x00007fa1f00e5000 nid=0x7bd7 in Object.wait() [0x00007fa1e5ca4000]
   java.lang.Thread.State: RUNNABLE
    at org.apache.log4j.Priority.<clinit>(Priority.java:45)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:169)
    at org.apache.commons.logging.impl.Log4JLogger.class$(Log4JLogger.java:37)
    at org.apache.commons.logging.impl.Log4JLogger.<clinit>(Log4JLogger.java:45)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
    at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactoryImpl.java:529)
    at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactoryImpl.java:235)
    at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactoryImpl.java:209)
    at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:351)
    at net.tcc.classloading.UseCommonsLoggingLogFactory.run(UseCommonsLoggingLogFactory.java:13)

"ReaderThread" prio=10 tid=0x00007fa1f00ed000 nid=0x7bd6 runnable [0x00007fa1e5da6000]
   java.lang.Thread.State: RUNNABLE
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.read(SocketInputStream.java:129)
    at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:264)
    at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306)
    at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158)
    - locked <0x00000007d7088a00> (a java.io.InputStreamReader)
    at java.io.InputStreamReader.read(InputStreamReader.java:167)
    at java.io.BufferedReader.fill(BufferedReader.java:136)
    at java.io.BufferedReader.readLine(BufferedReader.java:299)
    - locked <0x00000007d7088a00> (a java.io.InputStreamReader)
    at java.io.BufferedReader.readLine(BufferedReader.java:362)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner$ReaderThread.run(RemoteTestRunner.java:140)

"Low Memory Detector" daemon prio=10 tid=0x00007fa1f009d800 nid=0x7bd4 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" daemon prio=10 tid=0x00007fa1f009b800 nid=0x7bd3 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" daemon prio=10 tid=0x00007fa1f0098800 nid=0x7bd2 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" daemon prio=10 tid=0x00007fa1f0096800 nid=0x7bd1 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" daemon prio=10 tid=0x00007fa1f007a000 nid=0x7bd0 in Object.wait() [0x00007fa1e6c54000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000007d7001300> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:118)
    - locked <0x00000007d7001300> (a java.lang.ref.ReferenceQueue$Lock)
    at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:134)
    at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)

"Reference Handler" daemon prio=10 tid=0x00007fa1f0078000 nid=0x7bcf in Object.wait() [0x00007fa1e6d55000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000007d70011d8> (a java.lang.ref.Reference$Lock)
    at java.lang.Object.wait(Object.java:485)
    at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)
    - locked <0x00000007d70011d8> (a java.lang.ref.Reference$Lock)

"main" prio=10 tid=0x00007fa1f000c000 nid=0x7bc5 in Object.wait() [0x00007fa1f50b0000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x00000007d730dfd8> (a net.tcc.classloading.UseCommonsLoggingLogFactory)
    at java.lang.Thread.join(Thread.java:1186)
    - locked <0x00000007d730dfd8> (a net.tcc.classloading.UseCommonsLoggingLogFactory)
    at java.lang.Thread.join(Thread.java:1239)
    at net.tcc.classloading.TestMixedLoggerInstantiation.test(TestMixedLoggerInstantiation.java:21)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

"VM Thread" prio=10 tid=0x00007fa1f0071800 nid=0x7bce runnable 

"GC task thread#0 (ParallelGC)" prio=10 tid=0x00007fa1f001f000 nid=0x7bc6 runnable 

"GC task thread#1 (ParallelGC)" prio=10 tid=0x00007fa1f0021000 nid=0x7bc7 runnable 

"GC task thread#2 (ParallelGC)" prio=10 tid=0x00007fa1f0022800 nid=0x7bc8 runnable 

"GC task thread#3 (ParallelGC)" prio=10 tid=0x00007fa1f0024800 nid=0x7bc9 runnable 

"GC task thread#4 (ParallelGC)" prio=10 tid=0x00007fa1f0026800 nid=0x7bca runnable 

"GC task thread#5 (ParallelGC)" prio=10 tid=0x00007fa1f0028000 nid=0x7bcb runnable 

"GC task thread#6 (ParallelGC)" prio=10 tid=0x00007fa1f002a000 nid=0x7bcc runnable 

"GC task thread#7 (ParallelGC)" prio=10 tid=0x00007fa1f002c000 nid=0x7bcd runnable 

"VM Periodic Task Thread" prio=10 tid=0x00007fa1f00a8800 nid=0x7bd5 waiting on condition 

JNI global references: 1168

Воспроизведение тупика

Загрузите код из https://github.com/sebastiankirsch/classloading.
TestLoadingByClassForName должен легко вызвать тупик для вас (просто запустите его несколько раз), это является предварительным условием, что ваша система /JVM в конечном итоге перейдет в тупик для сложного сценария.

Теперь запустите TestMixedLoggerInstantiation несколько раз. Обратите внимание на среднее время выполнения, откройте UseLog4JLogger и установите сумму таймера сна на немного меньше, чем среднее время выполнения. Это в конечном итоге приведет к тупиковой ситуации, но это происходит редко.

Поэтому запустите его в командной строке следующим образом:

for (( ; ; )) do
  testExectution
done

Вместо того, чтобы вручную компоновать testExecution, просто установите разрывную цепочку в тесте, отлаживайте, введите ps -ef | grep TestMixedLoggerInstantiation в оболочку и скопируйте команду, которую использует ваша среда IDE.

Ответ 1

Наконец-то я нашел ответ в Спецификации языка Java, в частности в главе 12.4.2 Подробная процедура инициализации.
Там говорится:

[...]
2) Если объект класса для C указывает, что инициализация выполняется для C каким-либо другим потоком, тогда [...] заблокировать текущий поток, пока не сообщит, что завершенная инициализация завершена, [...]
7) Далее, если C - это класс, а не интерфейс, а его суперкласс SC еще не инициализирован, то рекурсивно выполнить всю эту процедуру для SC
10) Если выполнение инициализаторов завершено нормально, [...] пометьте объект класса для C как полностью инициализированный, уведомит все ожидающие потоки, [...]

Таким образом, поведение наблюдается точно так, как указано JLS. Я все еще немного озадачен тем, что нет никаких средств для обнаружения такого тупика. И почему потоки отмечены как RUNNABLE - но я думаю, это не ваш типичный байт-код, который должен быть выполнен, поэтому кто знает...

Как исправить проблему

Решение проблемы заключалось в том, чтобы избавиться от commons-logging. Как отметил @Robert Johnson, это можно легко сделать, используя org.slf4j:jcl-over-slf4j. Я также проверил код SLF: он не "использует" неудачный дизайн log4j.

Ответ 2

Ваш анализ правильный.
Вы можете попробовать выполнить свои параллельные тесты в разных загрузчиках классов, см. Обсуждение здесь о том, как это сделать. В Surefire и обсуждение в JUnit groups по этому вопросу.
В качестве обходного пути вы можете использовать org.apache.myfaces.test.runners.TestPerClassLoaderRunner, как описано в ссылке выше.