Статьи

Многопоточность в Java: от классических потоков до Virtual Threads

В Java многопоточность — это не факультатив, а основа производительных приложений. Любое веб-API, бэкенд для обработки тысяч запросов или вычислительный сервис так или иначе сталкивается с задачей параллельного выполнения.

Разберём ключевые инструменты — от базовых до современных.

Потоки: Thread, Runnable, Callable, Future

Всё начинается с класса Thread. Самый простой способ запустить код в отдельном потоке — создать объект и вызвать start():
Однако Thread быстро стал считаться слишком низкоуровневым. Вместо него обычно используют интерфейс Runnable для задания задачи и ExecutorService для управления пулом потоков.

Callable<T> — аналог Runnable, но с возможностью возвращать результат и пробрасывать исключения. Запускается через пул потоков, а результат оборачивается в Future<T>:

volatile — видимость изменений

volatile гарантирует, что чтение и запись переменной происходит напрямую из основной памяти, минуя кэш потока:
Это полезно для флагов и состояний, но не делает операции атомарными: counter++ всё равно небезопасен.

Atomic* — атомарные операции без блокировок

Классы вроде AtomicInteger используют под капотом CAS (Compare-And-Set), чтобы выполнять операции без блокировок:
Отлично подходит для счётчиков и флагов в условиях высокой конкуренции.

synchronized — монитор на страже

synchronized блокирует доступ к коду или методу через монитор (lock).

• Для обычных методов монитором является экземпляр класса (this).

• Для static методов — сам объект Class.

Блоки синхронизации можно оформлять явно:
Чем меньше синхронизированный блок, тем меньше риск задержек. Хорошая практика — синхронизировать только критический участок, а не целый метод.

Минус: при большом числе потоков они будут ждать, снижая производительность.
Плюс: простота и гарантированная корректность.

Пакет java.util.concurrent

Современный набор инструментов:

• ConcurrentHashMap — доступ без блокировки всей карты.

CopyOnWriteArrayList — копирование массива при изменении, чтение без блокировок.

• Executors — фабрики пулов потоков.

• Locks — гибкая альтернатива synchronized с таймаутами и попытками захвата.

ThreadLocal — данные на поток

ThreadLocal<T> хранит данные, уникальные для каждого потока. Это удобно для контекстов, например:
Важно очищать значения (remove()), чтобы избежать утечек в пуле потоков.

CompletableFuture — асинхронность без боли

CompletableFuture позволяет строить цепочки асинхронных задач:
Он избавляет от явного блокирования (get()), позволяя писать более реактивный код.

Virtual Threads и Scoped Values (Java 21+)

• Virtual Threads — лёгкие потоки, создаваемые тысячами без затрат системных потоков. Работают на планировщике и идеальны для I/O-задач.

• Scoped Values — альтернатива ThreadLocal, позволяющая безопасно передавать неизменяемые данные вниз по стеку вызовов в рамках жизненного цикла задачи.

Data Race и Race Condition

Data Race возникает, когда два потока одновременно читают и пишут одну переменную без синхронизации. Race Condition — более общее явление, когда результат зависит от порядка выполнения потоков.

Обе проблемы могут проявляться нестабильно — например, только под нагрузкой, что делает их особенно коварными.

Deadlock — тупик

Возникает, когда потоки ждут друг друга, блокируя ресурсы. Избежать можно, соблюдая порядок захвата, используя таймауты или минимизируя совместные блокировки.

Java предлагает широкий спектр инструментов: от простого synchronized до реактивных CompletableFuture и современных Virtual Threads.

Главная задача разработчика — выбрать подходящее решение, минимизировать блокировки и проектировать потоки так, чтобы они сотрудничали, а не мешали друг другу.


Хотите узнать больше? Изучите другие статьи из раздела:
Java