Concurrent
Что это
Это пакет в java.util предназначенный для работы с многопоточность.
Основные разделы
- Concurrent Collections - набор коллекций, более эффективно работающие в многопоточной среде нежели
стандартные универсальные коллекции.
- Queues - неблокирующие и блокирующие очереди с поддержкой многопоточности. Неблокирующие очереди заточены
на скорость и работу без блокирования потоков. Блокирующие очереди используются, когда нужно «притормозить»
потоки «Producer» или «Consumer», если не выполнены какие-либо условия, например, очередь пуста или
переполнена, или же нет свободного «Consumer»'a.
- Synchronizers - вспомогательные утилиты для синхронизации потоков.
- Executors - содержит в себе отличные фрейморки для
создания пулов потоков, планирования работы асинхронных задач с получением результатов.
- Locks - представляет собой альтернативные и более гибкие механизмы
синхронизации потоков по сравнению с базовыми synchronized, wait, notify, notifyAll.
- Atomics - классы с поддержкой атомарных операций над примитивами и ссылками.
Способы синхронизация
- wait/notify
- synchronized
- Thread.join
- Lock
- Semaphore
Synchronized
synchronized - ключевое слово, которое позволяет заблокировать доступ к методу или части кода(блок кода),
если его уже использует другой поток. По принципу Mutex. Все остальные потоки которые попробуют получить
монитор, станут wait(). После выхода из монитора вызывается - notify(). Не известно какой именно поток
с ожиданием запуститься.
Применения
- Для блока кода - если не нужно синхронизировать весь метод. Нужно передавать объект в качестве
монитора. Обычно передается this, это делает синхронизацию по текущему объекту.
private Object key = new Object();
synchronized (key) {
System.out.println("Hi I'm synchronized block!");
}
- Для метода. В качестве объекта будет текущая ссылка на объект(this).
synchronized void myMethod() {
System.out.println("Hi I'm synchronized method!");
}
Можно воспринимать так:
void myMethod() {
synchronized(this) {
System.out.println("Hi I'm synchronized method!");
}
}
Для статического метода передается ".class". По этому статическая блокировка и не статическая, на
одном классе не будут блокировать друг друга:
static void myMethod() {
synchronized(MyObject.class) {
System.out.println("Hi I'm synchronized method!");
}
}
Минус synchronized - другие потоки вынуждены ждать, пока нужный объект или метод освободится "bottle
neck". Если у объекта два синхронизированных метода, два потока не могут одновременно зайти в два
синхронизированных метода одного и того же объекта. Все потоки будут ждать освобождения общего лока.
Volatile
volatile - ключевое слово для переменной. Гарантирует видимость изменений между потоками.
Указывает что переменная ожидает изменения многими потоками.
Atomic - дают атомарные операции + видимость.
- Не блокирует другие потоки.
- Не используется для инкремента.
- Она всегда будет атомарно read/write(не про атомарность измений). Даже если это 64-битные
double или long. Несколько потоков не должны менять(edit) значение.
- Java-машина не будет помещать ее в кэш потоков(кеш процессора). Так что ситуация, когда 10 потоков
работают со своими локальными копиями этой переменной - исключена.
Условия использования volatile:
- Один поток меняет значение.
- Нет составных операций(изменения значения без изначального считывания\изменения).
//Можно:
flag = true;
int x = flag;
//Нельзя: так как 3 операции - чтение, измнение и запись.
count++; // НЕ безопасно!
Пример
public class VolatileExample {
private static volatile boolean flag = false;
public static void main(String[] args) {
// create and start a new thread
new Thread(() -> {
while (!flag) {
// do some work
}
System.out.println("Thread finished");
}).start();
// set the flag to true after a delay
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
}
}
Atomic
Пакет для атомарных(неделимые) изменений и операций, которые выполняются независимо и безопасно для
многопоточности, без использования volatile, synchronized или Lock, что уменьшает риск Deadlock или Race conditions.
Часто используется как счетчик/генератор уникальных ID.
Compare and Swap(CAS) - механизм который используют Atomic классы. который поддерживается на уровне процессора.
Часто быстрее чем синхронизация или блокировка(lock-free). Оптимистическая блокировка.
Внутри помечены слово volatile.
Как работает
- Читаем текущее значение.
- Инкриминируется значение.
- Перед commit, снова Берется значение.
- Сравнивается с полученным в начале.
- Если оно не было - изменение комитится. Если оно было - изменение операция повторяется(spin loop).
Виды:
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicIntegerArray
- AtomicLongArray
- AtomicReference
- AtomicReferenceArray
- AtomicStampedReference
- AtomicMarkableReference
Основные методы:
- get() - получить значение.
- set(value) - установить значение.
- getAndSet(value) - получить и сразу изменить.
- compareAndSet(expected, update) - изменить если значение равное ожидаемому.
- incrementAndGet() / decrementAndGet() - атомарное увеличение/уменьшение.
- getAndIncrement() / getAndDecrement() - атомарное увеличение/уменьшение с возращением старого значения.
- increment()
- getCounter()
public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // атомарное ++
}
public int get() {
return count.get();
}
}
ABA-проблема
Значение могло измениться на «A → B → A». Для простых счётчиков это не критично, но в сложных структурах
данных используют AtomicStampedReference. Необходимо только если значение может отниматься.
AtomicInteger когда использовать
- Для простых счётчиков, метрик.
- В неблокирующих алгоритмах (lock-free).
- В высоконагруженных сценариях, где synchronized слишком дорог.
Не подходит:
- Для сложных бизнес-операций над несколькими переменными (лучше использовать мьютексы или транзакции).
- При очень высокой конкуренции, может быть лучше взять LongAdder.
Virtual threads
Это лёгкие потоки, которые управляются самой JVM, а не операционной системой.
Потоки которые не требуют маппинга на реальные потоки процессора.
Легковесные потоки, которые позволяют писать синхронный код с производительностью асинхронного.
Классические Platform Threads имеют проблемы:
- Дорогие в создании (~1MB стека на поток).
- Ограничены числом (~тысячи потоков максимум).
- При блокировке (IO, sleep) поток простаивает, занимая ресурсы.
Virtual Threads
- Весят ~1KB.
- Создаются мгновенно.
- При блокировке платформенного потока освобождается для других задач.
- Позволяет обрабатывать миллионы конкурентных запросов.
Ключевые моменты
- Thread.ofVirtual() — создание билдера для virtual thread.
- Thread.startVirtualThread() — быстрый старт задачи.
- Executors.newVirtualThreadPerTaskExecutor() — пул для каждой задачи создаёт новый VT.
- Автоматическое отсоединение от платформенного потока при блокировке (IO, sleep, wait, park).
- Присоединение к платформенным потокам из ForkJoinPool.
Под капотом
- Когда виртуальный поток блокируется(например, на IO), он "отцепляется" от платформенного потока.
- Освободившийся платформенный поток берёт другой готовый виртуальный поток.
- Когда операция завершается, виртуальный поток "подцепляется" обратно к доступному платформенному потоку.
Подводные камни
Pinning (закрепление)
Virtual thread может "застрять" на платформенном потоке при:
- Synchronized блоках
- Нативных методах (JNI)
В таких случаях carrier thread блокируется вместе с virtual thread. Решение: использовать ReentrantLock
вместо synchronized.
ThreadLocal может быть опасен
Миллионы virtual threads с ThreadLocal приведут к огромному потреблению памяти. Используйте ScopedValue
(preview feature в Java 21+).
Не подходит для CPU-bound задач
Virtual threads оптимизированы для IO-bound операций. Для вычислений лучше параллельные стримы или
ForkJoinPool.
Мониторинг
Стандартные инструменты мониторинга потоков могут показывать некорректные данные — они заточены под platform
threads.
Когда использовать
- Высоконагруженные web-серверы с множеством конкурентных запросов.
- Микросервисы, которые взаимодействуют с API.
- Асинхронные операции, где ранее использовали CompletableFuture.
- Микросервисы с большим количеством внешних вызовов (HTTP, БД).
- Когда нужна простота синхронного кода без сложности реактивного.
- Замена больших thread pools для IO-операций.
Не подходит:
- При вычислениях на процессоре. CPU-intensive вычисления (сортировки, криптография).
- Код с большим количеством synchronized блоков(pinning).
- Легаси-код с активным использованием ThreadLocal.
Virtual Threads — мощный инструмент, который не заменяет потоки ОС, но отлично подходит для массовых
I/O-задач. Если у вас есть сервис, работающий с сетью, базами данных или API — пора внедрять Virtual
Threads.