Неблокирующие структуры данных: ConcurrentHashMap, LongAdder, ConcurrentLinkedQueue


Когда приложение начинает обрабатывать тысячи запросов в секунду, привычные synchronized-коллекции и простые атомарные счётчики перестают справляться. Они либо блокируют всю структуру целиком, либо заставляют потоки соревноваться за одну ячейку памяти, что приводит к падению пропускной способности.

В Java есть три мощные неблокирующие (или почти неблокирующие) структуры, которые помогают выжать максимум из многопоточного кода без перехода к реактивным фреймворкам и распределённым системам.


ConcurrentHashMap — главный кандидат для кэшей и справочников
Hashtable и Collections.synchronizedMap() защищают всю таблицу одной блокировкой. Это просто, но неэффективно при параллельном доступе.

ConcurrentHashMap устроена иначе. Внутри она использует сегментирование (в старых версиях) или CAS-операции и блокировку только на отдельной корзине (в версиях Java 8+). Это означает, что:

- чтение (get) практически никогда не блокируется;

- запись (put) затрагивает только одну корзину, остальные продолжают работать;

- методы вроде size() дают приблизительный результат и не должны использоваться для точной синхронизации.

Такой подход даёт отличную масштабируемость: при росте числа потоков производительность падает незначительно, в отличие от синхронизированных мап.

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

Важная деталь: метод computeIfAbsent может стать узким местом, если функция инициализации выполняется долго. В этом случае лучше использовать putIfAbsent с заранее подготовленным значением или применять ConcurrentHashMap.newKeySet() для множеств.


LongAdder — идеальный счётчик при высокой конкуренции
AtomicLong часто рекомендуют для счётчиков, но он имеет недостаток: все потоки соревнуются за одну и ту же переменную, что приводит к интенсивному обмену данными между кэшами процессоров (cache ping-pong). При 16 и более потоках производительность может упасть в несколько раз.

LongAdder решает эту проблему, разбивая счёт на несколько ячеек. Каждый поток пишет в свою ячейку, и общее значение получается суммированием всех ячеек. Это даёт значительный прирост при высокой конкуренции.

Пример использования:

LongAdder counter = new LongAdder();
// в потоках:
counter.increment();
// получение итога:
long total = counter.sum();

Метод sum() не даёт атомарного снимка — во время суммирования ячейки могут изменяться. Но для мониторинга и статистики такое приближение обычно допустимо. Если нужна строгая точность в каждый момент, стоит выбрать другой подход.
Когда использовать
: счётчики запросов, ошибок, времени выполнения, любые накопительные метрики.


ConcurrentLinkedQueue
— очередь без блокировок
В классических блокирующих очередях (LinkedBlockingQueue, ArrayBlockingQueue) потоки могут ждать освобождения ресурса. Это полезно в паттернах producer-consumer, но создаёт дополнительные накладные расходы и иногда приводит к задержкам.

ConcurrentLinkedQueue основана на неблокирующем алгоритме Майкла-Скотта и использует CAS для операций вставки и извлечения. Она никогда не блокирует потоки, что даёт более высокую пропускную способность, особенно при большом числе конкурентных производителей или потребителей.

Однако у неё есть особенности:
- нет ограничения ёмкости — очередь может бесконечно расти;
- метод size() требует обхода всей очереди и не рекомендуется;
- нельзя использовать блокирующие методы типа take() — только poll() и peek().

Когда использовать
: для буферизации событий, сбора логов, передачи задач в пул потоков, если не требуется ограничивать размер и блокироваться при пустоте.
Просмотры: 51
Открыть в MAX Сообщить о неточности

Репосты публикации

Публикация канала

Сообщить о неточности

Опишите, что нужно исправить. Если есть источник, добавьте ссылку.

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти