Многопоточность в 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    {// Наша очередь из сотрудников, наследник класса Thread
    private String[] names;

    PeopleQueue(String... names) {// Конструктор, аргумент- массив имен сотрудников
        this.names = names;
    }

    @Override
    public void run() { // Этот метод будет вызван при старте потока
        for (int i = 0; i < names.length; i++) { // Вывод в цикле с паузой 0.5 сек очередного сотрудника
            System.out.println("Обработаны документы: " + names[i]);
            try {
                sleep(500); // Задержка в 0.5 сек
            } 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. Полагаться на то, что «обычно выводится так», нельзя.

Читайте также