Поток (thread) — независимая последовательность выполнения кода внутри процесса Java-приложения, способная работать параллельно с другими потоками того же процесса. Разберем: чем поток отличается от процесса, из каких состояний состоит его жизненный цикл, тремя способами его создают, зачем нужна синхронизация и как правильно останавливать поток через interrupt — и что изменилось с приходом виртуальных потоков в Java 21.
Содержание
- Процесс и поток: в чем разница
- Жизненный цикл потока: 6 состояний Thread.State
- Три способа создать поток
- Синхронизация: как избежать состояния гонки
- Как останавливать поток: interrupt()
- Пакет java.util.concurrent: что внутри
- Виртуальные потоки в Java 21+: что изменилось к 2026 году
- Выводы
- Где применяется / связь с практикой
- FAQ
Процесс и поток: в чем разница
ОС запускает JVM как отдельный процесс со своим адресным пространством. Потоки внутри него работают в рамках памяти этого процесса, а не получают собственную копию.
| Критерий | Процесс | Поток |
|---|---|---|
| Память | Свое адресное пространство | Общая память процесса: куча, статические поля |
| Создание | Дороже — ОС выделяет отдельные ресурсы | Дешевле — использует ресурсы своего процесса |
| Обмен данными | Через межпроцессное взаимодействие (сокеты, файлы) | Напрямую через общие переменные и объекты |
| Сбой | Падение одного процесса не задевает другие | Необработанное исключение обычно завершает только этот поток |
Граница последней строки: если необработанное исключение вылетает из главного потока (main) и других non-daemon потоков не осталось, JVM-процесс завершается. В дочернем потоке необработанное исключение по умолчанию завершает только его.
Жизненный цикл потока: 6 состояний Thread.State
В Java состояние потока описывает перечисление Thread.State (метод getState()), и в нем ровно шесть значений:
| Состояние | Когда наступает |
|---|---|
| NEW | Объект создан, start() еще не вызван |
| RUNNABLE | После start(): выполняется либо ждет кванта времени от планировщика ОС |
| BLOCKED | Ждет освобождения монитора synchronized, занятого другим потоком |
| WAITING | Ждет неограниченно долго: wait()/join() без таймаута, LockSupport.park() |
| TIMED_WAITING | То же, но с ограничением по времени: sleep(long), wait(long), join(long) |
| TERMINATED | run() завершился — обычно или через исключение |
suspend(), resume() и stop() в этой модели не участвуют. Все три deprecated с JDK 1.2: stop() останавливал поток немедленно и мог оставить объект в неконсистентном состоянии, suspend() легко приводил к дедлоку, если поток держал монитор. С JDK 20 stop() дополнительно ре-специфицирован и безусловно бросает UnsupportedOperationException — то есть на актуальных LTS-версиях (21, 25) он уже не останавливает поток, а гарантированно падает с исключением. Правильный способ остановить поток — кооперативный, через interrupt() (ниже).
Три способа создать поток
Наследование Thread
public class ThreadClassDemo {
static class Worker extends Thread {
Worker(String name) {
super(name);
}
@Override
public void run() {
System.out.println("Поток " + getName() + " выполняет задачу");
}
}
public static void main(String[] args) throws InterruptedException {
Worker worker = new Worker("worker-1");
worker.start();
worker.join();
System.out.println("Главный поток завершен");
}
}
Результат:
Поток worker-1 выполняет задачу
Главный поток завершен
Порядок строк гарантирован вызовом join(): он заставляет главный поток дождаться worker. Без join() порядок вывода между независимыми потоками не гарантируется.
Реализация Runnable
Способ предпочтительнее наследования: Java допускает только одиночное наследование класса, а Runnable оставляет extends свободным для другой иерархии.
public class RunnableDemo {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> System.out.println(
"Поток " + Thread.currentThread().getName() + " выполняет задачу");
Thread worker = new Thread(task, "worker-2");
worker.start();
worker.join();
System.out.println("Главный поток завершен");
}
}
Результат:
Поток worker-2 выполняет задачу
Главный поток завершен
Callable + ExecutorService, когда нужен результат
У Runnable.run() тип возврата void, и он не бросает проверяемых исключений. Если из потока нужно получить значение или допустимо checked-исключение, используют Callable<V> с пулом ExecutorService.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableDemo {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
Callable<Integer> task = () -> {
Thread.sleep(50);
return 6 * 7;
};
Future<Integer> future = executor.submit(task);
System.out.println("Ждем результат вычисления...");
Integer result = future.get();
System.out.println("Результат: " + result);
executor.shutdown();
}
}
Результат:
Ждем результат вычисления...
Результат: 42
future.get() блокирует вызывающий поток до готовности результата, поэтому вывод детерминирован.
Синхронизация: как избежать состояния гонки
Состояние гонки возникает, когда несколько потоков без координации читают и изменяют одни и те же данные. Разберем на счетчике.
Неверный вариант:
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class RaceDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) {
counter.increment();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Итог: " + counter.getCount());
}
}
Ожидаем 200000. Но count++ — три отдельные операции (чтение, инкремент, запись), и без синхронизации потоки перезаписывают результат друг друга. Фактический итог меньше ожидаемого и меняется от запуска к запуску; иногда счетчик по совпадению доходит и до 200000, но это не доказывает корректность кода.
Исправление — пометить методы synchronized:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
Результат:
Итог: 200000
Теперь результат одинаков на каждом запуске: synchronized гарантирует, что increment() выполняет только один поток одновременно — он держит монитор объекта counter, остальные ждут в состоянии BLOCKED. Граница: блокируются только другие синхронизированные методы/блоки на ТОМ ЖЕ объекте-мониторе — несинхронизированный метод того же объекта выполнится параллельно.
Как останавливать поток: interrupt()
Способ показать потоку, что пора завершиться, — установить флаг прерывания через interrupt(). Поведение дальше зависит от того, что делает поток в этот момент.
Если поток работает в цикле без блокирующих вызовов, он должен сам проверять флаг:
public class InterruptLoopDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// полезная работа без блокирующих вызовов
}
System.out.println("Поток остановлен по флагу прерывания");
});
worker.start();
Thread.sleep(50);
worker.interrupt();
worker.join();
System.out.println("Главный поток завершен");
}
}
Результат:
Поток остановлен по флагу прерывания
Главный поток завершен
Если поток стоит в блокирующем вызове (sleep, wait, join), тот сразу бросает InterruptedException, а флаг прерывания при этом сбрасывается в false:
public class InterruptSleepDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
System.out.println("Поток прерван во время sleep()");
}
});
worker.start();
Thread.sleep(50);
worker.interrupt();
worker.join();
System.out.println("Главный поток завершен");
}
}
Результат:
Поток прерван во время sleep()
Главный поток завершен
Легко перепутать два похожих метода: isInterrupted() (метод экземпляра) только читает флаг; Thread.interrupted() (статический) читает флаг И сбрасывает его в false. Путаница — частый источник багов, когда цикл проверки перестает видеть прерывание.
Пакет java.util.concurrent: что внутри
Ручное управление потоками через new Thread() плохо масштабируется, поэтому на практике используют пакет java.util.concurrent:
- Executor framework (
ExecutorService,Executors,ScheduledExecutorService) — пулы потоков вместо создания потока на каждую задачу. - Concurrent-коллекции (
ConcurrentHashMap,CopyOnWriteArrayList,ConcurrentLinkedQueue) — потокобезопасны без блокировки всей структуры целиком. - Синхронизаторы (
CountDownLatch,CyclicBarrier,Semaphore) — координация группы потоков между собой. - Блокирующие очереди (
BlockingQueue,ArrayBlockingQueue,LinkedBlockingQueue) — основа схемы producer-consumer. - Locks (
ReentrantLock,ReadWriteLockизjava.util.concurrent.locks) — гибчеsynchronized: явныеlock()/unlock(),tryLock()с таймаутом. - Atomic-классы (
AtomicInteger,AtomicLong,AtomicReference) — атомарные операции поверх compare-and-swap без блокировки.
Atomic хорошо решает гонку для одного счетчика, но не заменяет Lock/synchronized, если нужно согласованно менять сразу несколько полей объекта.
Виртуальные потоки в Java 21+: что изменилось к 2026 году
Java 21 (LTS, сентябрь 2023) добавила виртуальные потоки (JEP 444) — легковесные потоки под управлением JVM, а не ОС; создаются через Thread.ofVirtual(). Тысячи виртуальных потоков обслуживает пул carrier-потоков ОС: по умолчанию его размер равен числу доступных ядер CPU (настраивается свойством jdk.virtualThreadScheduler.parallelism), а не фиксированным «один-два» вне зависимости от железа. API тот же Thread (те же состояния, interrupt(), join()), отличие — в цене создания и планировании: годятся для массовой конкурентности в I/O, а не для ускорения CPU-вычислений. В Java 21-23 виртуальный поток дополнительно «пришпиливался» к своему carrier на время выполнения synchronized-блока/метода; с JDK 24 (JEP 491, релиз вне LTS-линейки, март 2025) это ограничение сняли. Вторая действующая LTS-линейка после 21 — Java 25 (вышла 16 сентября 2025); на дату публикации актуальны обе.
Выводы
- Поток — единица выполнения внутри процесса; потоки одного процесса делят его память (кучу, статические поля), в отличие от процессов.
- У потока 6 состояний по
Thread.State: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED. - Три способа создать поток:
extends Thread,implements Runnable(предпочтительно),Callable+ExecutorService(когда нужен результат или проверяемое исключение). - Без синхронизации совместный доступ к изменяемым данным дает состояние гонки;
synchronized,Lockи Atomic-классы решают ее на разной глубине гибкости. - Останавливать поток нужно кооперативно через
interrupt()и проверку флага, а не через deprecatedThread.stop().
Где применяется / связь с практикой
Освойте тему на практике
Тема потоков и Concurrency — частый блок на технических собеседованиях Java-разработчика, а на практике она нужна везде, где приложение обслуживает несколько запросов одновременно: веб-бэкенды, очереди сообщений, параллельная агрегация данных. Разобрать механику потоков, пулы ExecutorService и актуальные подходы, включая виртуальные потоки, можно на курсе Java Developer. Basic — там многопоточность разбирают на практических задачах с разбором типичных ошибок.
Освойте тему на практике
По темам многопоточности и параллельного программирования регулярно проходят открытые уроки — можно оценить формат занятий еще до записи на курс.
Смежные темы: События в Java
FAQ
Чем классический поток отличается от виртуального с точки зрения ОС?
Классический поток (platform thread) — обертка над потоком ОС: создание и переключение контекста дороже, число таких потоков ограничено тысячами. Виртуальный поток — объект JVM, и тысячи таких объектов может обслуживать относительно небольшой пул carrier-потоков ОС (по умолчанию размером с число ядер CPU).
Может ли поток создать другой поток?
Да, любой поток может внутри своего run() или call() создать и запустить новый Thread. Иерархии родитель-потомок здесь нет: все потоки одного процесса равноправны и делят его память.
Что будет, если вызвать start() у одного и того же объекта Thread дважды?
Второй вызов бросит IllegalThreadStateException: поток уже вышел из состояния NEW, повторно запустить тот же объект нельзя — нужен новый экземпляр.



