В Java многопоточность — это не факультатив, а основа производительных приложений. Любое веб-API, бэкенд для обработки тысяч запросов или вычислительный сервис так или иначе сталкивается с задачей параллельного выполнения.
Разберём ключевые инструменты — от базовых до современных.
Разберём ключевые инструменты — от базовых до современных.
Потоки: Thread, Runnable, Callable, Future
Всё начинается с класса Thread. Самый простой способ запустить код в отдельном потоке — создать объект и вызвать start():
Однако Thread быстро стал считаться слишком низкоуровневым. Вместо него обычно используют интерфейс Runnable для задания задачи и ExecutorService для управления пулом потоков.
Callable<T> — аналог Runnable, но с возможностью возвращать результат и пробрасывать исключения. Запускается через пул потоков, а результат оборачивается в Future<T>:
Callable<T> — аналог Runnable, но с возможностью возвращать результат и пробрасывать исключения. Запускается через пул потоков, а результат оборачивается в Future<T>:
volatile — видимость изменений
volatile гарантирует, что чтение и запись переменной происходит напрямую из основной памяти, минуя кэш потока:
Это полезно для флагов и состояний, но не делает операции атомарными: counter++ всё равно небезопасен.
Atomic* — атомарные операции без блокировок
Классы вроде AtomicInteger используют под капотом CAS (Compare-And-Set), чтобы выполнять операции без блокировок:
Отлично подходит для счётчиков и флагов в условиях высокой конкуренции.
synchronized — монитор на страже
synchronized блокирует доступ к коду или методу через монитор (lock).
• Для обычных методов монитором является экземпляр класса (this).
• Для static методов — сам объект Class.
Блоки синхронизации можно оформлять явно:
• Для обычных методов монитором является экземпляр класса (this).
• Для static методов — сам объект Class.
Блоки синхронизации можно оформлять явно:
Чем меньше синхронизированный блок, тем меньше риск задержек. Хорошая практика — синхронизировать только критический участок, а не целый метод.
Минус: при большом числе потоков они будут ждать, снижая производительность.
Плюс: простота и гарантированная корректность.
Минус: при большом числе потоков они будут ждать, снижая производительность.
Плюс: простота и гарантированная корректность.
Пакет java.util.concurrent
Современный набор инструментов:
• ConcurrentHashMap — доступ без блокировки всей карты.
• CopyOnWriteArrayList — копирование массива при изменении, чтение без блокировок.
• Executors — фабрики пулов потоков.
• Locks — гибкая альтернатива synchronized с таймаутами и попытками захвата.
• 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, позволяющая безопасно передавать неизменяемые данные вниз по стеку вызовов в рамках жизненного цикла задачи.
• Scoped Values — альтернатива ThreadLocal, позволяющая безопасно передавать неизменяемые данные вниз по стеку вызовов в рамках жизненного цикла задачи.
Data Race и Race Condition
Data Race возникает, когда два потока одновременно читают и пишут одну переменную без синхронизации. Race Condition — более общее явление, когда результат зависит от порядка выполнения потоков.
Обе проблемы могут проявляться нестабильно — например, только под нагрузкой, что делает их особенно коварными.
Обе проблемы могут проявляться нестабильно — например, только под нагрузкой, что делает их особенно коварными.
Deadlock — тупик
Возникает, когда потоки ждут друг друга, блокируя ресурсы. Избежать можно, соблюдая порядок захвата, используя таймауты или минимизируя совместные блокировки.
Java предлагает широкий спектр инструментов: от простого synchronized до реактивных CompletableFuture и современных Virtual Threads.
Главная задача разработчика — выбрать подходящее решение, минимизировать блокировки и проектировать потоки так, чтобы они сотрудничали, а не мешали друг другу.
Хотите узнать больше? Изучите другие статьи из раздела:
Главная задача разработчика — выбрать подходящее решение, минимизировать блокировки и проектировать потоки так, чтобы они сотрудничали, а не мешали друг другу.
Хотите узнать больше? Изучите другие статьи из раздела: