- Вопрос или проблема
- Суть
- Предыстория
- Пример
- Полный запуск приложения
- Дамп потоков?
- Ссылки
- Ответ или решение
- Какие типы потоков использует Java/JVM с точки зрения операционной системы Linux?
- Понимание потоков Java
- Нативные потоки и POSIX Threads
- Как потоки Java отображаются в 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.
Ссылки
- Существует ли способ увидеть детали всех потоков, которые есть у процесса в Linux?
- Linux: Как просмотреть потоки процесса
- man ps
- man 7 pthreads
- Небольшое демонстрационное приложение, показывающее, как потоки Java сопоставляются с потоками Linux через JNI
- Java JNI
- Как потоки Java сопоставляются с потоками ОС?
- Как найти поток Java, выполняющийся на Linux с помощью ps -axl?
Ответ или решение
Какие типы потоков использует 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, чтобы получить полную картину того, как ваше приложение использует доступные ресурсы.