Да будет Thread.
Краткое (нет) руководство по потокам в java.
Этой статьей я начинаю цикл статей про базовые концепции языка Java без которых мало того что нельзя пройти собеседование ни в одну более-менее адекватную компанию, но и стыдно не знать таких основополагающих вещей.
Оглавление
Потоки в java (под микроскопом)
Введение
В современных операционных системах существует два ключевых понятия - поток и процесс. Дабы не сильно вдаваться в подробности реализации потоков и процессов на различных операционных системах, скажу что процессы относятся к потокам в отношении один ко многим.
Иными словами, один процесс может порождать множество потоков.
Наглядно отношение изображено ниже на рисунке 1.
Для того чтобы в дальнейшем не было путаницы будем называть их потоками и процессами операционной системы.
Теперь давайте перейдем к потокам в java.
Потоки в java
Поток в java представлен классом java.lang.Thread. Все, что происходит в вашей программе происходит в рамках какого-то потока. При старте программы создается главный поток, в котором происходит исполнение и метода main и остального кода программы. В этом же потоке могут создаваться другие потоки.
В дальнейшем потоки вашей программы будем называть java-потоками.
Начиная с jdk1 и до jdk18 каждый java-поток соотносился 1 к 1 с потоком операционной системы. Начиная с java 19 появилась возможность создавать так называемые зеленые потоки из-за чего соотношение превратилось в 1 ко многим.
Справедливости ради стоит отметить, что вместе с первыми версиями jdk поставлялась библиотека с зелеными потоками, но уже к версии 1.3 руководство тогда еще Sun решили избавиться от нее и сделали выбор в пользу потоков в том виде, в котором мы их знаем сегодня - 1 поток операционной системы, 1 java-поток [1].
Наглядно отношение java-потоков к потокам операционной системы изображено ниже на рисунке ниже.
Потоки в java (под микроскопом)
Создать поток очень просто - достаточно создать экземпляр класса Thread и вызвать у него метод start().
Thread myVeryFirstThread = new Thread(); myVeryFirstThread.start();
Это просто и известно всем. Но что же происходит когда мы запускаем наш поток? Хорошо что jdk снабжена исходным кодом и мы всегда можем посмотреть его в любимой IDE. Итак, давайте взглянем на метод start():
В целом все понятно - происходит
- Проверка состояния потока (переменная threadStatus содержит число - 0, поток в состоянии NEW, число 1 - поток в состоянии RUNNABLE, число 2, 3 - поток TERMINATED, подробно маппинг можно посмотреть в методе jdk.internal.misc.VM#toThreadState).
- Добавление в группу потоков (не беспокойтесь - переменная group будет инициализирована, даже если вы вызвали конструктор без аргументов).
- Собственно старт потока в методе start0.
А вот с методом start0 уже сложнее, потому что у него есть модификатор native. Native методы реализуются на языке платформы - как правило C++ - и посмотреть их исходный код, не зная где они находятся наверняка, сложно.
Для того чтобы найти что конкретно делает метод start0 необходимо
- Клонировать к себе репозиторий openjdk [2]
- Найти файл jvm.cpp
- Понять, что из этого файла вызывается файл Thread.cpp
Бросить все и пойти в простит- Выучить синтаксис С++ чтобы понять что там происходит
Если кресты учить не очень хочется, то вот краткое содержание того что происходит при создании потока НА САМОМ ДЕЛЕ:
- Аллокация памяти для стека потока (параметр задан по умолчанию в 256КБ, либо передается в аргументах конструктора класса Thread).
- Создается объект JavaThread (еще не платформоспецифичный враппер объекта потока в рантайме)
- Создается объект OSThread (здесь поток уже платформоспецифичный)
- Выполняется подготовка потока - выставление приоритета, например
- Если до этого момента не случилось ошибок, то состояние потока меняется на RUNNABLE
- Выполняется запуск потока на уровне операционной системы - выполняются системные вызовы, поток создан и готов к выполнению работы
В данном сценарии очень хорошо просматривается философия Write Once, Run Everywhere. С точки зрения рядового разработчика создать поток ничем не будет отличаться ни на Windows, ни на Lunix, ни на Solaris. Вы просто создаете поток, а JVM сама заботится о том какие специфичные вызовы сделать к операционной системе, какие особенные флаги добавить, как аллоцировать память. На входе у вас всегда будет java-поток, абстракция представленная классом java.lang.Thread, но для операционной системы виртуальная машина предоставит понятный для нее поток.
Стоит отметить, что вышеописанный сценарий взят на базе исходного кода Hotspot Openjdk, так что имейте в виду - у других вендоров реализация может отличаться.
Напоследок хочу отметить, что создание потока - вещь дюже дорогая не только на уровне операционной системы.
Класс Thread усыпан модификаторами synchronized - а это значит, что перед каждой такой операцией либо будет захватываться объект монитора, либо проверяться на возможность захвата, а по завершении операции объект монитора будет отпускаться. Захват/освобождение монитора весьма дорогостоящая вычислительная операция.
Также, если взглянуть на C++ код JVM, то по ходу выполнения кода создания потока постоянно захватывается и отпускается мьютекс, что тоже не добавляет быстродействия. И это мы еще не вышли за пределы JVM.
Именно поэтому появилась идея использовать пул потоков.
Впрочем, об этом в следующей части.