Статьи

Обнаружение потоков Java в тупике с помощью Groovy и JMX

К сожалению, приложения Java, использующие преимущества нескольких потоков, могут время от времени сталкиваться с ужасной тупиковой ситуацией . К счастью, платформа Java делает обнаружение взаимоблокировки относительно простым. Фактически, встроенный (начиная с J2SE 5 ) ThreadMXBean ( PlatformManagedObject, предоставляемый через JMX ) делает эту информацию доступной любому клиенту, который «говорит на JMX » через методы findDeadlockedThreads () и findMonitorDeadlockThreads () . Общие « JMX- клиенты», такие как JConsole и VisualVM, используют это для предоставления информации об обнаруженных взаимоблокировках, но можно написать собственные инструменты и сценарии для предоставления тех же деталей. В этой статье я расскажу об использовании Groovy в сочетании с API-интерфейсом Attach для обнаружения заблокированных потоков для локально выполняющегося процесса JVM.

Java Tutorials предоставляет простой и довольно интересный пример кода Java, который обычно приводит к тупику на странице « Deadlock » в уроке « Concurrency » из цикла « Essential Classes ». Я привел этот пример здесь только в малейшей адаптированной форме в качестве кода, с которым я буду запускать клиенты JMX для обнаружения тупиковой ситуации. (В качестве примечания я продолжаю видеть сообщения на reddit / Java и других онлайн-форумах, в которых просят найти лучшие бесплатные онлайновые вводные ресурсы для изучения Java; я не могу придумать лучшего ответа на этот вопрос, чем Учебные руководства по Java.)

Deadlock.java (адаптировано из обучающих программ по Java )

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package dustin.examples.jmx.threading;
 
import static java.lang.System.out;
 
/**
 * Example of a class that often will lead to deadlock adapted from the <em>
 * Java Tutorials</em> "Concurrency" section on "Deadlock":
 */
public class Deadlock
{
   static class Friend
   {
      /** Friend's name. */
      private final String name;
 
      /**
       * Parameterized constructor accepting name for Friend instance.
       * @param newName Name of new Friend instance.
       */
      public Friend(final String newName)
      {
         this.name = newName;
      }
       
      /**
       * Provide this instance's name.
       *
       * @return Friend's name.
       */
      public String getName()
      {
         return this.name;
      }
 
      /**
       * Bow from friend. Synchronized for thread-safe access.
       *
       * @param bower Friend that is bowing.
       */
      public synchronized void bow(final Friend bower)
      {
         out.format("%s: %s has bowed to me!%n",
            this.name, bower.getName());
         bower.bowBack(this);
      }
 
      /**
       * Bow back to friend who bowed to me. Synchronized for thread-safe access.
       *
       * @param bower Friend who has bowed back to me.
       */
      public synchronized void bowBack(final Friend bower)
      {
         out.format("%s: %s  has bowed back to me!%n",
            this.name, bower.getName());
      }
   }
 
   /**
    * Simple executable function that demonstrates deadlock when two friends
    * are waiting on each other to bow to finish bowing.
    *
    * @param arguments Command-line arguments: none expected.
    */
   public static void main(final String[] arguments)
   {
      final Friend alphonse = new Friend("Alphonse");
      final Friend gaston = new Friend("Gaston");
      new Thread(new Runnable()
      {
         public void run() { alphonse.bow(gaston); }
      }, "Gaston Bowing").start();
      new Thread(new Runnable()
      {
         public void run() { gaston.bow(alphonse); }
      }, "Alphonse Bowing").start();
   }
}

На следующем снимке экрана показано, что это простое приложение заходит в тупик между двумя потоками.

Даже если бы мы не знали, что это приложение предназначено для демонстрации тупика, тот факт, что оно никогда не заканчивается или не изменяет состояние, является хорошим намеком на то, что оно зашло в тупик. Мы можем использовать JMX, чтобы определить это наверняка. Независимо от того, используем ли мы JMX-клиент, jstack или какой-либо другой механизм для определения взаимоблокировки, нам, вероятно, понадобится pid (идентификатор процесса) процесса Java, поэтому следующий снимок экрана указывает на это (pid в данном случае 3792) с обоими jps и jcmd (последний доступен только после JDK 7).

Зная, что pid — 3792, я могу запустить ‘jconsole 3794’ в командной строке, чтобы JConsole загрузилась с информацией об этом заблокированном Java-процессе. В этом случае меня интересуют потоки, и на следующем изображении показана вкладка «Потоки» в JConsole для этого демонстрационного приложения тупика.

Сразу на предыдущем снимке экрана обведены две интересующие темы. В моем адаптированном примере кода я назвал их «Gaston Bowing» и «Alphonse Bowing», чтобы их было легче найти в JConsole и других инструментах. Если бы я не предоставил эти явные имена в примере (чего не сделал первоначальный пример в Учебниках Java), на них ссылались бы более общие имена «Thread-0» и «Thread-1». С загруженной JConsole и доступной вкладкой «Потоки» я могу нажать на кнопку «Определить тупики», чтобы увидеть информацию в следующих двух снимках экрана (по одному для двух выбранных потоков).

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

Java VisualVM можно запустить с помощью команды jvisualvm а затем можно выбрать рассматриваемый процесс Java, как показано на следующем снимке экрана. Обратите внимание, что VisualVM автоматически обнаруживает взаимоблокировки на вкладке « Потоки » VisualVM и выдает сообщение красным шрифтом, указывающее на это.

Когда я нажимаю кнопку «Дамп потока», как указано, чтобы увидеть, какие потоки заблокированы, создается дамп потока . Внизу этого дампа потока, как показано на следующем снимке экрана, находятся детали, окружающие заблокированные потоки.

Как и JConsole, VisualVM также указывает в предоставленном дампе потока, что два изгибающихся потока ожидают друг друга, и для каждого из потоков, определенных как участвующих в смертельном объятии, предоставляется «информация стека Java».

JConsole и Java VisualVM упрощают идентификацию потоков, участвующих в взаимоблокировке. Однако, когда инструмент командной строки предпочтительнее инструмента на основе графического интерфейса, jstack является очевидным выбором. Снова используя pid, определенный ранее для этого конкретного процесса (в моем случае 3792), можно просто набрать jstack 3792 чтобы увидеть дамп потока для этого приложения. Следующие два снимка экрана показывают часть этого с первым снимком, показывающим выполняемую команду и начало вывода, и вторым снимком, показывающим часть, связанную с взаимоблокировкой.

Не нужно слишком внимательно присматриваться, чтобы понять, что вывод jstack такой же, как и в «дампе потока» в VisualVM.

JConsole и VisualVM обеспечивают хороший подход с графическим интерфейсом для определения взаимоблокировки, а jstack делает то же самое из командной строки. Учитывая эти инструменты, можно задаться вопросом, почему мы заботимся о том, чтобы иметь возможность создавать наши собственные инструменты и сценарии для того же. Часто этих инструментов достаточно, но бывают случаи, когда я могу захотеть сделать что-то с информацией, как только получу ее, или хочу, чтобы ответ был еще более прямым, чем эти инструменты. Если мне нужно программно что-то сделать с информацией или просто захотеть идентифицировать потоки, участвующие в взаимоблокировке без подробностей других потоков, может потребоваться специальный инструмент. Остальная часть этого поста посвящена этому.

В следующем листинге кода содержится код Groovy для печати информации о потоках, связанной с заблокированными потоками, как предусмотрено ThreadMXBean . В этом коде используется фрагмент кода Groovy, который здесь не показан ( JmxServer использует Attach API), который можно найти в моем последнем сообщении в блоге .

displayDetectedDeadlock.groovy

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env groovy
def pid = args[0]
def javaThreads = new javax.management.ObjectName("java.lang:type=Threading")
def server = JmxServer.retrieveServerConnection(pid)
long[] deadlockedThreadIds = server.invoke(javaThreads, "findDeadlockedThreads", null, null)
deadlockedThreadIds.each
{ threadId ->
   Object[] parameters = [threadId, 10]
   String[] signature = [Long.TYPE, Integer.TYPE]
   def threadInfo = server.invoke(javaThreads, "getThreadInfo", parameters, signature)
   print "\nThread '${threadInfo.threadName}' [${threadInfo.threadId}] is "
   print "${threadInfo.threadState} on thread '${threadInfo.lockOwnerName}' ["
   println "${threadInfo.lockOwnerId}]:\n"
   println "Java stack information for the threads listed above:"
   println "==================================================="
   println "'${threadInfo.threadName}':"
   threadInfo.stackTrace.each
   { compositeData ->
      print "\tat ${compositeData.className}.${compositeData.methodName}("
      println "${compositeData.fileName}:${compositeData.lineNumber})"
   }
   println "\n\n"
}

Вышеуказанный скрипт Groovy задает 10 как максимальную глубину для информации трассировки стека, поскольку он предоставляет детали, аналогичные тем, которые представлены в JConsole, VisualVM и jstack. Конечно, преимущество этого подхода состоит в том, что любые из этих данных, которые здесь выводятся на стандартный вывод, также могут использоваться при принятии решений во время выполнения и обрабатываться программно из кода Java или Groovy. На следующем снимке экрана показаны результаты выполнения этого сценария для заблокированного приложения, которое использовалось ранее в этом посте.

Java облегчает обнаружение потоков, заблокированных в смертельном объятии, известном как взаимоблокировка. Хотя общих инструментов, таких как JConsole, VisualVM и jstack, часто достаточно для выявления этих случаев, приятно иметь возможность писать собственные сценарии и инструменты для того же самого. Это позволяет разработчикам гибко включать обнаружение взаимоблокировок непосредственно в свои сценарии и инструменты, а не анализировать вывод jstack или прибегать к другим методам очистки данных .