Многопоточность в Java это выполнение нескольких потоков (
Thread) внутри одного процесса: потоки делят память процесса и могут работать параллельно на разных ядрах. Поток создается из класса
Thread или задачи
Runnable и запускается методом
start(), а его код живет в методе
run(). Главные темы вокруг потоков: их состояния, потоки-демоны и синхронизация доступа к общим данным (
учебник Oracle: процессы и потоки). Ниже аналогия с отделом кадров, определения и первая многопоточная программа.
Кратко
- Процесс это запущенная программа со своей изолированной памятью; поток это путь выполнения внутри процесса, и все потоки процесса делят его память.
- Java-программа начинается в главном потоке с методом
main и завершается, когда закончился последний не-daemon поток; потоки-демоны при этом обрываются.
- У потока шесть состояний (
Thread.State): NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED.
- Поток создают наследованием
Thread (код в run()) или через Runnable и запускают только методом start(); прямой вызов run() нового потока не создает.
- Порядок работы потоков задает планировщик ОС, поэтому вывод меняется от запуска к запуску, а доступ к общим данным нужно синхронизировать.
- Oracle рекомендует передавать задачу как
Runnable, а с Java 21 есть виртуальные потоки (JEP 444) для массовых задач с ожиданием ввода-вывода.
Введение
Прежде чем узнать про потоки Java, давайте заглянем в недалекое будущее. Представьте, что вы подали резюме и прошли собеседование. Вас и пару дюжин будущих коллег пригласили на работу в большую Software-компанию. Среди прочих хлопот нужно подать бумажные документы для трудоустройства уставшему сотруднику HR-отдела.
Чтобы ускорить процесс, претендентов на должность можно разделить на две группы и распределить их между двумя HR-менеджерами (если таковые есть в компании). В результате мы получаем ускорение процесса за счет параллельной (
parallel) работы по оформлению.
Если же кадровик в компании один, то придется как-то выкручиваться. Например, можно снова-таки разбить всех на две группы, например, собеседовать поочередно девушек и юношей.
Или по другому принципу: так как в нижней группе больше народа, будем чередовать на одного юношу двух девушек.
Такой способ организации работы называется
многопоточным. Наш утомленный кадровик переключается на разные группы для оформления из них очередного сотрудника. Групп, может быть, одиннадцать, а кадровиков четыре. В этом случае многопоточная (
multithreading) обработка будет происходить параллельно несколькими HR-ами, которые могут брать очередного человека из любой группы для обработки его документов.
Процессы
Процессом (
process) в данном случае будет организация работы приема документов. В организации можно выделить несколько процессов: бухгалтерский учет, разработка ПО, встречи с клиентами, работа склада и т. д. На каждый процесс выделены ресурсы: помещение, сотрудники для его исполнения.
Процессы изолированы друг от друга: у кадровиков отсутствует доступ в бухгалтерскую базу, а менеджеры по работе с клиентами не бегают по складу. Если процесс должен получить доступ к чужим ресурсам, необходимо наладить межпроцессное взаимодействие: служебные записки, совместные совещания.
Потоки
Работа в процессе организована в виде потоков (
java thread). Для отдела кадров поток это организация работы по обслуживанию группы. На самой первой картинке один поток, на последующих трех два. Внутри процесса потоки могут выполняться параллельно: два кадровика принимают две или более группы будущих сотрудников.
Порядок, в котором кадровик переключается между группами, то есть распределение времени между потоками внутри процесса, называют
планированием потоков (scheduling). На рисунках оформления одним кадровиком двух групп видны два способа: равномерный (девушка, юноша, девушка, юноша) и с разными приоритетами (две девушки чередуются с одним юношей). А согласование доступа потоков к общим для них вещам называют
синхронизацией потоков, и о ней следующий абзац.
Потоки имеют доступ к ресурсам процесса, к которому они относятся: группам к кадровику даны образцы бланков заявлений, ручки для заполнения документов. Но если потоки взаимодействуют с общими для них вещами, то возможны казусы. Если кадровик попросит крикнуть имя последнего человека в очереди, то, в случае с двумя группами, он не уверен заранее, что услышит женское имя или мужское. Подобные конфликты доступа к данным, блокировки и способы их разрешения это очень важная тема.
Состояния потока
Каждый поток пребывает в одном из следующих состояний (state):
- Создан (
New): очередь к кадровику готовится, люди организуются.
- Запущен (
Runnable): наша очередь выстроилась к кадровику и обрабатывается.
- Заблокирован (
Blocked): последний в очереди юноша пытается выкрикнуть имя, но услышав, что девушка в соседней группе начала делать это раньше него, замолчал.
- Завершен (
Terminated): вся очередь оформилась у кадровика и в ней нет необходимости.
- Ожидает (
Waiting): одна очередь ждет сигнала от другой.
- Ожидает с таймаутом (
Timed Waiting): очередь ждет ограниченное время, скажем, пока кадровик пьет кофе ровно пять минут; в это состояние попадает поток после sleep(500) из примера ниже.
Всего состояний шесть, все они перечислены в документации Oracle в перечислении
Thread.State, а текущее состояние потока возвращает метод
getState(). Организация потоков и их взаимодействие это основа эффективной работы процессов.
Вернемся в IT-мир
В XXI веке многопоточное и параллельное выполнение стало актуальным. С 90-х годов прошлого века многозадачные операционные системы Windows, macOS и Linux прочно обосновались на домашних компьютерах. В них часто можно встретить четырех- и более ядерные процессоры. Число параллельных блоков GPU-видеокарт уже перевалило за тысячу. Популярные программы пишутся с учетом многопоточности (multithreading), например, современные версии ПО обработки графики, видео или оперирующих большим объемом данных: Adobe Photoshop, WinRAR, Mathematica, современные игры.
Многопоточность Java это очень важная, востребованная и сложная тема. Поэтому в курсе JavaRush встречается много задач, чтобы разобраться с ней очень хорошо. Java-примеры на многопоточность помогут освоить основные нюансы и тонкости этой области, синхронизации работы потоков.
Процесс в терминах ОС
Process (процесс) это выполняющийся экземпляр программы, которому Операционная Система (ОС) выделила память, процессорное время/ядра и прочие ресурсы. Важно, что память выделяется отдельно, адресные пространства различных процессов недоступны друг другу. Если процессам необходимо обмениваться данными, они могут это сделать с помощью файлов, каналов и иных способов межпроцессного взаимодействия.
Поток (Thread) в Java
Java
Thread (поток). Иногда, чтобы не путать с другими классами Java,
Stream и подобными, потоки Java часто переводят как нить. Они используют выделенные для процесса ресурсы и являются способом выполнения процесса. Главный поток выполняет метод
main и завершается. При выполнении процесса могут порождаться дополнительные потоки (дочерние). Потоки одного процесса могут между собой обмениваться данными. Многопоточность Java требует учитывать синхронизацию данных, не забывайте об этом.
В Java процесс завершается тогда, когда закончил работу последний его поток. Для фоновых задач поток можно запустить как
демон (
daemon), отличие которого от обычного в том, что они будут принудительно завершены при окончании работы всех не-
daemon потоков процесса.
Процесс и поток: в чем разница
Сведем оба понятия в одну таблицу:
| Признак |
Процесс |
Поток |
| Что это |
выполняющийся экземпляр программы |
путь выполнения внутри процесса |
| Память |
своя, изолирована от других процессов |
общая с остальными потоками процесса |
| Обмен данными |
файлы, каналы, сокеты (межпроцессное взаимодействие) |
общие объекты в памяти, поэтому нужна синхронизация |
| Стоимость создания |
высокая: ОС выделяет адресное пространство и ресурсы |
низкая, а у виртуальных потоков Java 21 почти нулевая |
| Когда завершается |
когда закончился последний не-daemon поток |
когда закончился метод run() |
| В аналогии статьи |
отдел кадров с помещением и сотрудниками |
очередь у конкретного кадровика |
Первое многопоточное приложение
Существует более полудюжины способов создания потоков, в рамках JavaRush курса мы их подробно разберем. Для начала познакомимся с одним из базовых.
Имеется специальный класс
Thread, в методе
run() которого необходимо написать код, реализующий логику программы. После создания потока можно запустить его, вызвав метод
start().
Напишем демонстрационную программу, реализующую пример многопоточности Java.
class PeopleQueue extends Thread {
private String[] names;
PeopleQueue(String... names) {
this.names = names;
}
@Override
public void run() {
for (int i = 0; i < names.length; i++) {
System.out.println("Обработаны документы: " + names[i]);
try {
sleep(500);
} catch (Exception e) {}
}
}
}
public class HR {
public static void main(String[] args) {
PeopleQueue queue1 = new PeopleQueue("Иван","Сергей","Николай","Фердинанд","Василий");
PeopleQueue queue2 = new PeopleQueue("Мария","Людмила","Алиса","Карина","Ольга");
System.out.println("Начали!");
queue1.start();
queue2.start();
}
}
Запустим программу. В консоли виден вывод сообщения главным потоком. Далее, каждый дочерний поток
queue1 и
queue2 поочередно выводят сообщения в общую для них консоль об очередном обработанном сотруднике. Один из возможных вариантов работы программы:
Начали!
Обработаны документы: Мария
Обработаны документы: Иван
Обработаны документы: Людмила
Обработаны документы: Сергей
Обработаны документы: Алиса
Обработаны документы: Николай
Обработаны документы: Карина
Обработаны документы: Фердинанд
Обработаны документы: Ольга
Обработаны документы: Василий
Process finished with exit code 0
Наследование от
Thread это самый наглядный, но не самый ходовой способ.
Документация Oracle называет более общим вариант с
Runnable: класс задачи не привязан к
Thread, а с лямбдой это одна строка:
new Thread(() -> System.out.println("Привет из потока")).start();. А с Java 21 в языке есть виртуальные потоки (
JEP 444):
Thread.startVirtualThread(задача) создает легкий поток, таких потоков можно запустить миллионы, и это удобно для серверных задач, которые большую часть времени ждут ответа от сети или диска.
Многопоточность в Java это тема трудная и многосторонняя. Умение писать код с использованием параллельных, многозадачных и многопоточных вычислений поможет вам эффективно реализовать задачи на современных многоядерных процессорах и кластерах, состоящих из множества компьютеров.
Вопросы и ответы
Чем поток отличается от процесса?
Процесс это запущенная программа со своей памятью, которую операционная система изолирует от других процессов. Поток это путь выполнения внутри процесса: все потоки процесса делят его память и ресурсы, поэтому обмениваются данными напрямую, но именно из-за этого нуждаются в синхронизации. Одна JVM это один процесс, а внутри нее работают главный поток с методом
main и все потоки, которые вы создали.
В чем разница между start() и run()?
start() просит JVM создать новый поток и уже в нем вызвать
run(). Если вызвать
run() напрямую, нового потока не появится: код выполнится в текущем потоке как обычный метод. В примере статьи замена
queue1.start() на
queue1.run() напечатала бы все имена по порядку и без чередования. Вызвать
start() у одного объекта
Thread можно только один раз, повторный вызов бросит
IllegalThreadStateException.
Какие состояния есть у потока в Java?
Шесть, они перечислены в
Thread.State:
NEW (создан, но не запущен),
RUNNABLE (выполняется или готов выполняться),
BLOCKED (ждет монитор, занятый другим потоком),
WAITING (ждет сигнала без таймаута, например после
wait() или
join()),
TIMED_WAITING (ждет ограниченное время, например в
sleep(500)) и
TERMINATED (метод
run() завершился). Текущее состояние возвращает
thread.getState().
Что такое поток-демон?
Служебный поток для фоновых задач, который не мешает программе завершиться: JVM выходит, как только закончились все обычные (не-daemon) потоки, а демоны обрываются принудительно. Демоном поток делает вызов
setDaemon(true) до
start(). Пример из самой JVM: сборщик мусора. Не поручайте демонам запись в файлы или базу данных: их могут остановить на полпути.
Как правильно создавать поток: наследовать Thread или реализовать Runnable?
Документация Oracle называет
Runnable более общим способом: класс задачи остается свободным для наследования от чего угодно, а сама задача отделена от механизма запуска. С лямбдой это одна строка:
new Thread(() -> работа).start(). Наследование от
Thread, как в примере статьи, уместно для учебных программ. В реальных приложениях потоки обычно не создают вручную, а отдают задачи пулу
ExecutorService или, начиная с Java 21, виртуальным потокам.
Почему вывод многопоточной программы меняется от запуска к запуску?
Потому что порядок выполнения потоков определяет планировщик операционной системы, а не ваш код: у него свои кванты времени, приоритеты и загрузка ядер. Оба потока в примере пишут в общую консоль, и кто успеет первым, заранее неизвестно. Если порядок важен, его задают явно: через
join(), синхронизацию или очереди из
java.util.concurrent. Полагаться на то, что «обычно выводится так», нельзя.
Читайте также