Какие типы потоков использует Java/JVM с точки зрения операционной системы Linux?

Вопрос или проблема

Недавно мой друг-разработчик задал вопрос: Как потоки Java приложения, работающего на системе Linux, отображаются в операционной системе Linux?

Что такое потоки Java?

Суть

В Java 1.1 зеленые потоки были единственной моделью многопоточности, используемой виртуальной машиной Java (JVM),9 по крайней мере на Solaris. Поскольку зеленые потоки имеют некоторые ограничения по сравнению с нативными потоками, последующие версии Java отказались от них в пользу нативных потоков. 10,11.

Источник: Зеленые потоки

Ниже приведена иллюстрация, показывающая, как анализировать потоки Java с точки зрения операционной системы.


Предыстория

Изучая этот вопрос, я наткнулся на этот вопрос и ответ на SO с заголовком: Использует ли Java JVM pthread?
. В рамках этого вопроса была ссылка на исходный код JVM – OpenJDK / jdk8u / jdk8u / hotspot. В частности, этот раздел:

    // Сериализовать создание потоков, если мы выполняем с фиксированным стеком LinuxThreads
    bool lock = os::Linux::is_LinuxThreads() && !os::Linux::is_floating_stack();
    if (lock) {
      os::Linux::createThread_lock()->lock_without_safepoint_check();
    }

    pthread_t tid;
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);

    pthread_attr_destroy(&attr);

Здесь мы видим, что JVM использует pthread, или потоки POSIX. Дополнительные детали из man-страницы pthreads(7):

POSIX.1 определяет набор интерфейсов (функций, заголовочных файлов) для
многопоточного программирования, обычно известного как потоки POSIX или Pthreads.
Один процесс может содержать несколько потоков, все из которых выполняют одну и ту же программу.
Эти потоки разделяют одну и ту же глобальную память (данные и сегменты кучи), но каждый поток имеет свой собственный стек (автоматические переменные).

Учитывая это, потоки в Java просто являются pthread в Linux.

Пример

Эксперимент

Чтобы further подтвердить это, мы можем использовать следующий пример приложения Scala, которое в конечном итоге является приложением Java.

Это приложение запускается в контейнере Docker, но мы можем использовать его для изучения работающего Java приложения, которое использует потоки. Чтобы использовать это приложение, мы просто клонируем репозиторий Git, а затем строим и запускаем контейнер Docker.

Сборка приложения

$ git clone https://github.com/slmingol/jvmthreads.git
$ cd jvmthreads
$ docker build -t threading .
$  docker run -it -v ~/.coursier/cache:/root/.cache/coursier -v ~/.ivy2:/root/.ivy2  -v ~/.sbt:/root/.sbt -v ~/.bintray:/root/.bintray -v $(pwd):/threading threading:latest /bin/bash

На этом этапе вы должны находиться внутри контейнера Docker с подобным приглашением:

root@27c0fa503da6:/threading#

Запуск приложения

Отсюда вы захотите запустить приложение sbt:

$ sbt compile compileCpp "runMain com.threading.ThreadingApp"

Когда это приложение начнет выполняться, вы можете использовать Ctrl+Z, чтобы SIGSTP приложение, чтобы мы могли его изучить.

root@27c0fa503da6:/threading#  sbt compile compileCpp "runMain com.threading.ThreadingApp"
[info] Загружаю настройки из metaplugins.sbt ...
[info] Загружаю определение проекта из /threading/project/project
[info] Загружаю настройки из plugins.sbt ...
[info] Загружаю определение проекта из /threading/project
[info] Загружаю настройки из build.sbt ...
[warn] Отсутствуют учетные данные bintray. Либо создайте файл учетных данных с помощью задачи bintrayChangeCredentials, установите переменные окружения BINTRAY_USER и BINTRAY_PASS или передайте свойства bintray.user и bintray.pass в sbt.
[warn] Отсутствуют учетные данные bintray. Либо создайте файл учетных данных с помощью задачи bintrayChangeCredentials, установите переменные окружения BINTRAY_USER и BINTRAY_PASS или передайте свойства bintray.user и bintray.pass в sbt.
^Z
[1]+  Остановлено                 sbt compile compileCpp "runMain com.threading.ThreadingApp"

Анализ приложения

Отсюда мы можем использовать типичные инструменты UNIX, такие как ps, чтобы увидеть, как приложение под тестом ведет себя с точки зрения операционной системы.

Стандартная команда ps показывает ваше типичное выполняемое приложение.

root@27c0fa503da6:/threading# ps -eaf
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 02:14 pts/0    00:00:00 /bin/bash
root      1503     1  0 02:37 pts/0    00:00:00 bash /usr/bin/sbt compile compileCpp runMain com.threading.ThreadingApp
root      1571  1503 98 02:37 pts/0    00:00:35 java -Xms1024m -Xmx1024m -XX:ReservedCodeCacheSize=128m -XX:MaxMetaspaceSize=256m -jar /usr/share/sbt/bin/sbt-launch.jar
root      1707  1571  0 02:37 pts/0    00:00:00 git describe --tags --abbrev=8 --match v[0-9]* --always --dirty=+20191026-0237
root      1718     1  0 02:37 pts/0    00:00:00 ps -eaf

Просмотр потоков с помощью ps показывает более полную картину:

root@27c0fa503da6:/threading# ps -eLf | head -8
UID        PID  PPID   LWP  C NLWP STIME TTY          TIME CMD
root         1     0     1  0    1 02:14 pts/0    00:00:00 /bin/bash
root      1943     1  1943  0    1 03:08 pts/0    00:00:00 bash /usr/bin/sbt compile compileCpp runMain com.threading.ThreadingApp
root      2011  1943  2011  0   32 03:08 pts/0    00:00:00 java -Xms1024m -Xmx1024m -XX:ReservedCodeCacheSize=128m -XX:MaxMetaspaceSize=256m -jar /usr/share/sbt/bin/sbt-launch.jar compile compileCpp runMain com.threading.ThreadingApp
root      2011  1943  2012  0   32 03:08 pts/0    00:00:05 java -Xms1024m -Xmx1024m -XX:ReservedCodeCacheSize=128m -XX:MaxMetaspaceSize=256m -jar /usr/share/sbt/bin/sbt-launch.jar compile compileCpp runMain com.threading.ThreadingApp
root      2011  1943  2013  0   32 03:08 pts/0    00:00:00 java -Xms1024m -Xmx1024m -XX:ReservedCodeCacheSize=128m -XX:MaxMetaspaceSize=256m -jar /usr/share/sbt/bin/sbt-launch.jar compile compileCpp runMain com.threading.ThreadingApp
root      2011  1943  2014  0   32 03:08 pts/0    00:00:00 java -Xms1024m -Xmx1024m -XX:ReservedCodeCacheSize=128m -XX:MaxMetaspaceSize=256m -jar /usr/share/sbt/bin/sbt-launch.jar compile compileCpp runMain com.threading.ThreadingApp
root      2011  1943  2015  0   32 03:08 pts/0    00:00:00 java -Xms1024m -Xmx1024m -XX:ReservedCodeCacheSize=128m -XX:MaxMetaspaceSize=256m -jar /usr/share/sbt/bin/sbt-launch.jar compile compileCpp runMain com.threading.ThreadingApp

ПРИМЕЧАНИЕ: Выше мы видим, что существует множество потоков. Колонки, которые представляют интерес в этом представлении, – это LWP и NLWP.

  • LWP обозначает потоки легковесных процессов
  • NLWP обозначает количество LWP

Число NLWP имеет значение, поскольку оно указывает общее количество потоков, связанных с PID. В нашем случае это число 32. Вы можете подтвердить это так:

root@27c0fa503da6:/threading# ps -eLf|grep -E "[3]2.*java" | wc -l
32

Вы также можете использовать эти команды ps, чтобы получить альтернативные способы проверки этих потоков:

root@27c0fa503da6:/threading# ps -Lo pid,lwp,pri,nice,start,stat,bsdtime,cmd,comm | head -5
  PID   LWP PRI  NI  STARTED STAT   TIME CMD                         COMMAND
    1     1  19   0 02:14:42 Ss     0:00 /bin/bash                   bash
 1943  1943  19   0 03:08:41 T      0:00 bash /usr/bin/sbt compile c bash
 2011  2011  19   0 03:08:41 Tl     0:00 java -Xms1024m -Xmx1024m -X java
 2011  2012  19   0 03:08:41 Tl     0:05 java -Xms1024m -Xmx1024m -X java

ПРИМЕЧАНИЕ1: Эта форма показывает, что это потоки pthread, благодаря l в колонке STAT.

  • S – прерываемый сон (ожидание завершения события)
  • T – остановлено сигналом управления заданиями
  • l – многопоточный (использует CLONE_THREAD, как делают pthread NPTL)

ПРИМЕЧАНИЕ2: S и T имеют значение здесь, поскольку указывают на то, что этот процесс был остановлен через Ctrl+Z с помощью сигнала управления SIGSTP.

Вы также можете использовать переключатель ps -T, чтобы просмотреть их как потоки:

root@27c0fa503da6:/threading# ps -To pid,tid,tgid,tty,time,comm | head -5
  PID   TID  TGID TT           TIME COMMAND
    1     1     1 pts/0    00:00:00 bash
 1943  1943  1943 pts/0    00:00:00 bash
 2011  2011  2011 pts/0    00:00:00 java
 2011  2012  2011 pts/0    00:00:05 java

Выше указанные переключатели ps:

   -L     Показать потоки, возможно, с колонками LWP и NLWP.
   -T     Показать потоки, возможно, с колонкой SPID.

Полный запуск приложения

Для справки вот полный запуск приложения Scala/Java, если вам интересно.

root@27c0fa503da6:/threading# sbt compile compileCpp "runMain com.threading.ThreadingApp"
[info] Загружаю настройки из build.sbt ...
[warn] Отсутствуют учетные данные bintray. Либо создайте файл учетных данных с помощью задачи bintrayChangeCredentials, установите переменные окружения BINTRAY_USER и BINTRAY_PASS или передайте свойства bintray.user и bintray.pass в sbt.
[warn] Отсутствуют учетные данные bintray. Либо создайте файл учетных данных с помощью задачи bintrayChangeCredentials, установите переменные окружения BINTRAY_USER и BINTRAY_PASS или передайте свойства bintray.user и bintray.pass в sbt.
[info] Установите текущий проект на threading (в файле сборки: /threading/)
[info] Выполняется в пакетном режиме. Для лучшей производительности используйте оболочку sbt
[warn] Файл учетных данных /root/.bintray/.credentials не существует, игнорируется
[success] Общее время: 2 с, завершено 26 октября 2019 года в 4:11:38 AM
[success] Общее время: 1 с, завершено 26 октября 2019 года в 4:11:39 AM
[warn] Файл учетных данных /root/.bintray/.credentials не существует, игнорируется
[info] Запуск (fork) com.threading.ThreadingApp
[info] Запущен linux поток 140709608359680!
[info] Запущен linux поток 140709599966976!
[info] Запуск  thread_entry_pointЗапущен linux поток 140709591574272!
[info] Запуск  thread_entry_pointЗапущен linux поток 140709583181568!
[info] Выполняется Поток 1
[info] Запуск  thread_entry_pointЗапущен linux поток 140709369739008!
[info] Выполняется Поток 2
[info] Запуск  thread_entry_pointЗапущен linux поток 140709608359680!
[info] Выполняется Поток 3
[info] Запуск  thread_entry_pointЗапущен linux поток 140709599966976!
[info] Выполняется Поток 4
[info] Выполняется Поток 5Запуск  thread_entry_pointЗапущен linux поток 140709361346304!
[info] Выполняется Поток 6
[info] Запуск  thread_entry_pointЗапущен linux поток 140709583181568!
[info] Запущен linux поток 140709591574272!
[info] Запуск  thread_entry_pointЗапущен linux поток 140709352953600!
[info] Выполняется Поток 7
[info] Выполняется Поток 9
[info] Запущен linux поток 140709369739008!
[info] Запуск  thread_entry_pointЗапущен linux поток 140709608359680!
[info] Выполняется Поток 8
[info] Запуск  thread_entry_pointЗапущен linux поток 140709344560896!
[info] Запуск  thread_entry_pointЗапущен linux поток 140709583181568!
[info] Запуск  thread_entry_pointЗапущен linux поток 140709599966976!
[info] Запуск  thread_entry_pointЗапущен linux поток 140709336168192!
[info] Выполняется Поток 10
[info] Выполняется Поток 11
[info] Запуск  thread_entry_pointЗапущен linux поток 140709327775488!
[info] Выполняется Поток 12Запущен linux поток 140709591574272!
[info] Выполняется Поток 13
[info] Выполняется Поток 14
[info] Выполняется Поток 16
[info] Выполняется Поток 15
[info] Выполняется Поток 18
[info] Выполняется Поток 17
[info] Выполняется Поток 19
[info] Запуск  thread_entry_pointЗапуск  thread_entry_point
[success] Общее время: 1 с, завершено 26 октября 2019 года в 4:11:40 AM

Дамп потоков?

Некоторые спрашивали, как мы можем связать то, что поток Java эквивалентен LWP в Linux. Для этого мы можем использовать дамп потоков Java для сравнения двух.

Снова мы используем то же приложение Scala выше, и мы собираемся Ctrl+Z.

root@52a4b6e78711:/threading# sbt compile compileCpp "runMain com.threading.ThreadingApp"
[info] Загружаю настройки из metaplugins.sbt ...
[info] Загружаю определение проекта из /threading/project/project
^Z
[1]+  Остановлено                 sbt compile compileCpp "runMain com.threading.ThreadingApp"

После этого нам нужно отправить SIGQUIT в JVM. Для этого вы обычно можете использовать kill -3 <PID JVM>:

root@52a4b6e78711:/threading# ps -eaf
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 12:36 pts/0    00:00:00 /bin/bash
root         7     1  0 12:37 pts/0    00:00:00 bash /usr/bin/sbt compile compileCpp runMain com.threading.ThreadingApp
root        75     7 99 12:37 pts/0    00:00:17 java -Xms1024m -Xmx1024m -XX:ReservedCodeCacheSize=128m -XX:MaxMetaspaceSize=256m -jar /usr/share/sbt/bin/sbt-launch.jar
root       130     1  0 12:37 pts/0    00:00:00 ps -eaf

root@52a4b6e78711:/threading# kill -3 75

Затем нам нужно разрешить программе возобновиться, fg:

root@52a4b6e78711:/threading# fg
sbt compile compileCpp "runMain com.threading.ThreadingApp"
2019-10-26 12:38:00
Полный дамп потоков OpenJDK 64-Bit Server VM (25.181-b13 mixed mode):

"scala-execution-context-global-32" #32 daemon prio=5 os_prio=0 tid=0x00007f87d8002800 nid=0x80 runnable [0x00007f880973d000]
   java.lang.Thread.State: ОЖИДАНИЕ (парковка)
    at sun.misc.Unsafe.park(Native Method)
    - парковка, чтобы ждать <0x00000000c1b08a68> (a scala.concurrent.impl.ExecutionContextImpl$$anon$3)
    at java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1824)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1693)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

"scala-execution-context-global-33" #33 daemon prio=5 os_prio=0 tid=0x00007f87dc001000 nid=0x7f runnable [0x00007f880983e000]
   java.lang.Thread.State: ОЖИДАНИЕ ВРЕМЕНИ (парковка)
    at sun.misc.Unsafe.park(Native Method)
    - парковка, чтобы ждать <0x00000000c1b08a68> (a scala.concurrent.impl.ExecutionContextImpl$$anon$3)
    at java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1824)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1693)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

"scala-execution-context-global-31" #31 daemon prio=5 os_prio=0 tid=0x00007f87d8001000 nid=0x7e ждет условия [0x0000000000000000]
   java.lang.Thread.State: ВЫПОЛНЯЕТСЯ

"scala-execution-context-global-30" #30 daemon prio=5 os_prio=0 tid=0x00007f87e4003800 nid=0x7d ждет условия [0x00007f8809a40000]
   java.lang.Thread.State: ОЖИДАНИЕ (парковка)
    at sun.misc.Unsafe.park(Native Method)
    - парковка, чтобы ждать <0x00000000c1b08a68> (a scala.concurrent.impl.ExecutionContextImpl$$anon$3)
    at java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1824)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1693)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

ПРИМЕЧАНИЕ: В приведенном выше частичном выводе команды kill -3 вы можете увидеть, что потоки в JVM соответствуют нашему анализу. Их 32, что демонстрирует, что потоки Java на самом деле 1:1 с LWP в Linux.

Ссылки

Ответ или решение

Какие типы потоков использует Java/JVM с точки зрения операционной системы Linux?

При запуске Java-приложения на системе Linux, которое делает использование потоков, важно понимать, как эти потоки взаимодействуют с операционной системой. Вопрос о том, какие именно потоки создает Java, зачастую вызывает интерес у разработчиков. Рассмотрим этот вопрос более детально.

Понимание потоков Java

Потоки в Java представляют собой основную единицу выполнения программы. Каждое приложение может содержать несколько потоков, которые могут выполняться параллельно, что позволяет использовать ресурсы системы более эффективно. С момента появления Java 1.1, реализация потоков претерпела значительные изменения. Ранее использовавшиеся «green threads» были заменены на нативные потоки, что открыло новые возможности для многопоточного программирования.

Нативные потоки и POSIX Threads

Согласно исходному коду OpenJDK, JVM использует POSIX Threads (pthreads) при создании новых потоков. Это создаёт связь между потоками Java и потоками на уровне операционной системы (LWP, или Light Weight Processes). На уровне нативной реализации, каждый поток Java становится отдельным потоком (или LWP) в Linux, что позволяет операционной системе управлять ими так, как если бы они были нативными потоками.

Примерный код создания потока в JVM:
pthread_t tid;
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);

Этот фрагмент кода иллюстрирует, как JVM вызывает функции pthread для создания новых потоков, обеспечивая интеграцию со средой выполнения операционной системы.

Как потоки Java отображаются в Linux

На Linux, вы можете использовать различные инструменты для анализа потоков Java. Например, следующая команда ps позволяет посмотреть, как потоки выглядят с точки зрения ОС:

ps -eLf

Эта команда отображает все потоки в процессе, включая их состояние и идентификаторы. В выводе команды есть столбцы:

  • LWP — Light Weight Process (идентификатор потока)
  • NLWP — Total Number of Lightweight Processes (общее количество потоков в процессе)

Количество потоков можно проверить так:

ps -eLf | grep <PID_Java>

Пример анализа потоков

Для иллюстрации приведем пример с приложением, использующим потоки. Мы можем запустить приложение на Scala (которое является Java-приложением) и проанализировать его:

sbt compile "runMain com.threading.ThreadingApp"

После выполнения приложения можно использовать Ctrl+Z, чтобы остановить его, а затем ps -eLf для наблюдения за состоянием потоков. Вы сможете увидеть, как Java-потоки отображаются как pthreads.

Различия с древними моделями потоков

Следует отметить, что в ранних версиях Java использовались так называемые «green threads», которые не имели прямой связи с потоками операционной системы и выполнялись в одном потоке. Это ограничивало возможность использования многоядерных процессоров. Современная реализация, использующая нативные потоки, позволяет Java-приложениям более эффективно использовать многопоточность, что значительно повышает их производительность на многоядерных архитектурах.

Заключение

Таким образом, современные Java-потоки при выполнении на операционной системе Linux представляют собой нативные потоки, эквивалентные POSIX pthreads. Это обеспечивает эффективное управление потоками на уровне операционной системы и позволяет разработчикам Java реализовывать многопоточность более эффективно, оптимизируя производительность приложения.

Для глубокого понимания и анализа потоков в Java всегда полезно комбинировать изучение Java API и инструменты анализа потоков в Linux, чтобы получить полную картину того, как ваше приложение использует доступные ресурсы.

Оцените материал
Добавить комментарий

Капча загружается...