Понимаем многопоточность в Java через коллекции и атомики

1️⃣ HashMap / TreeMap / TreeSet (не потокобезопасные)

HashMap:
  • Структура: массив бакетов + связные списки / деревья (для коллизий).
  • Под капотом: при put/remove происходит модификация массива бакетов и, возможно, переупорядочивание цепочек.
  • Проблема при многопоточности: два потока могут одновременно менять один бакет → corrupt структуры, потеря элементов, бесконечный цикл при итерации.
TreeMap / TreeSet:
  • Структура: красно-черное дерево.
  • При вставке/удалении поток изменяет несколько узлов дерева.
  • Одновременные изменения → структура дерева может сломаться, get/put/rebalance могут выбросить ConcurrentModificationException или повредить балансировку.

Вывод: нужна внешняя синхронизация, иначе карта/множество легко портится.

2️⃣ SynchronizedMap / SynchronizedSortedMap / SynchronizedSortedSet

Под капотом: Все методы обёрнуты в synchronized (this) на объекте карты/сета. Любая операция блокирует всю карту/сет на время выполнения. Гарантирует целостность данных, но нет параллельной работы.

public V put(K key, V value) {
    synchronized (this) {
        return m.put(key, value);
    }
}

Чтение/запись → одна операция одновременно, что исключает race condition.

3️⃣ ConcurrentHashMap

Под капотом (Java 8+):

  • Массив бакетов + узлы в деревьях для коллизий.
  • Основная синхронизация:
    • get — полностью без блокировок.
    • put — CAS (Compare-And-Swap) на уровне бакета или synchronized на узле дерева, только если нужно.
    • Расширение таблицы (resize) частично синхронизировано на сегментах.
  • Одновременные put/get на разные ключи — выполняются параллельно без блокировки всей карты.
  • Race condition на значении: если это изменяемый объект внутри карты, CHM не синхронизирует его поля.

4️⃣ ConcurrentSkipListMap / ConcurrentSkipListSet

  • Структура: Skip List (многослойный связный список).
  • Операции используют lock-free алгоритмы + CAS.
  • Каждый уровень списка частично блокируется или обновляется атомарно.
  • Позволяет параллельные операции на разных ключах.
  • Одновременные изменения одного ключа → race condition, если не использовать атомарные методы (compute, putIfAbsent).

5️⃣ AtomicInteger / AtomicReference / AtomicLong

  • Используют CPU-инструкции CAS (Compare-And-Swap).
  • Обновление атомарное и lock-free: одно и то же значение не потеряется при одновременных потоках.
  • Пример:
    AtomicInteger ai = new AtomicInteger(0);
    ai.incrementAndGet(); // атомарно
  • Без блокировок на уровне JVM или объекта — чистый CAS на CPU.

Таблиц для сравнения: один элемент, два потока

Коллекция / объект Поток 1 → Поток 2 пришёл чуть позже Поток 1 ↔ Поток 2 начали одновременно Под капотом / комментарий
HashMap ❌ race condition → значение может потеряться, структура в порядке (если только одно поле) ❌ race condition → может сломать структуру, потеря данных Нет синхронизации, всё на усмотрение потоков CPU
TreeMap / TreeSet ❌ race condition → corrupt дерева или потеря данных ❌ race condition → corrupt дерева Нет потокобезопасности, манипуляции с узлами не атомарны
SynchronizedMap / SynchronizedSortedMap / SynchronizedSortedSet ✅ безопасно, поток 2 ждёт ✅ безопасно, один поток ждёт другого Монитор блокирует всю карту/сет на операцию
ConcurrentHashMap ⚠️ структура карты в порядке, но значение race condition: последнее присвоение побеждает ⚠️ CAS на уровне бакета или узла → последнее присвоение побеждает Структура карты lock-free, поле объекта не защищено
ConcurrentSkipListMap / Set ⚠️ структура skip list корректна, значение race condition ⚠️ последнее присвоение побеждает CAS на уровне узлов, объект значения не атомарен
AtomicInteger / AtomicReference / AtomicLong ✅ атомарно, поток 2 ждёт в CAS только при конфликте, значение увеличивается корректно ✅ атомарно, CAS гарантирует одно обновление → второе повторяет попытку CPU CAS, lock-free, race condition исключён
Теперь рассмотрим более сложный кейс с одновременным изменением двумя потоками и при этом добавляется новый ключ третьим потоком.

Таблица: один элемент редактируется двумя потоками + новый ключ добавляется

Коллекция / объект Пример кода Поток 1+2 редактируют элемент Поток 3 добавляет новый ключ Под капотом / комментарий Потеря данных / corrupt
HashMap
static Map<String,Integer> map = new HashMap<>();
❌ race condition, одно обновление может потеряться ❌ добавление нового ключа может сломать структуру Нет синхронизации, все потоки одновременно меняют бакеты Высокий риск corrupt
TreeMap / TreeSet
static TreeMap<String,Integer> map = new TreeMap<>();
❌ race condition, структура дерева может сломаться ❌ добавление нового ключа меняет узлы дерева Нет потокобезопасности, балансировка дерева ломается Высокий риск corrupt
SynchronizedMap / SynchronizedSortedMap / SynchronizedSortedSet
static Map<String,Integer> map = Collections.synchronizedMap(new HashMap<>());
✅ Поток 1 и 2 выполняются последовательно (блокировка на мониторе) ✅ Поток 3 ждёт завершения Все методы синхронизированы на объекте, итерация требует блока Низкий, безопасно
ConcurrentHashMap
static ConcurrentHashMap<String,AtomicInteger> map = new ConcurrentHashMap<>(); 
map.computeIfAbsent("key", k -> new AtomicInteger()).incrementAndGet();
⚠️ Race condition на значении, структура карты корректна ✅ Добавление нового ключа параллельно Lock-free бакеты + CAS, чтение lock-free Значение может потеряться, структура в порядке
ConcurrentSkipListMap / Set
static ConcurrentSkipListMap<String,AtomicInteger> map = new ConcurrentSkipListMap<>();
⚠️ Race condition на значении ✅ Добавление нового ключа параллельно Skip list + CAS, lock-free Значение может потеряться, структура в порядке
AtomicInteger / AtomicReference
AtomicInteger ai = new AtomicInteger(); 
ai.incrementAndGet();
✅ атомарно, оба потока корректно обновляют значение ✅ добавление нового ключа через отдельный объект AtomicReference безопасно CAS на уровне CPU, lock-free Низкий, безопасно

🔹 Ключевые моменты:

  • SynchronizedMap — методы блокируют весь объект, поэтому Поток 1 и Поток 2 никогда не выполняются одновременно.
  • ConcurrentHashMap — структура карты безопасна, но значение объекта не атомарно, поэтому нужен AtomicInteger или метод compute.
  • Atomic объекты — полностью атомарно, race condition исключено.

Сравнение потокобезопасных отсортированных коллекций

1️⃣ Collections.synchronizedSortedMap(new TreeMap<>())
Collections.synchronizedSortedMap(new TreeMap<>())
Поток A: ─────[взял монитор]─────────────> работает
Поток B: ─────[ждёт, монитор занят]─────┐
Поток C: ─────[ждёт, монитор занят]─────┘
Все операции блокируют друг друга: get, put, remove, containsKey — только один поток внутри одновременно.
Вывод:
Реальное дерево (красно-чёрное).
Полная сортировка сохраняется.
Но нет параллельности — все ждут.

2️⃣ ConcurrentSkipListMap
Level 3: 10 --------- 30 ---------------- 75 --------- 90
Level 2: 10 ---- 25 ---- 50 ---- 60 ---- 75 ---- 90
Level 1: 10  20  25  30  40  50  60  70  75  80  90

Поток A: вставляет 35
Поток B: вставляет 55
Поток C: читает 30

✔ Все три операции могут выполняться одновременно, потому что блокировки берутся только на маленькие сегменты списка.
✔ Порядок ключей сохраняется, субмапы работают.

Вывод:
Не дерево, а skip-list.
Отсортированные ключи как в TreeMap.
Многопоточно, параллельно, без глобальной блокировки.

Свойство SynchronizedSortedMap(TreeMap) ConcurrentSkipListMap
Структура Красно-чёрное дерево Skip-list
Порядок ключей Да Да
Потокобезопасность Да, через synchronized Да, через fine-grained локи
Параллельная работа Нет, один поток Да, несколько потоков одновременно
Поддержка subMap/headMap Да Да
Производительность при многих потоках Падает Хорошая

🔹 Выводы

HashMap / TreeMap / TreeSet

  • Не потокобезопасны.
  • Любые одновременные изменения могут привести к потере данных, повреждению структуры и бесконечным итерациям.
  • Требуется внешняя синхронизация (например, synchronized блок).

SynchronizedMap / SynchronizedSortedMap / SynchronizedSortedSet

  • Потокобезопасны, но все операции блокируют всю коллекцию.
  • Хорошо для нечастых операций, но плохо для высокой конкурентной нагрузки.
  • Итерация требует отдельной синхронизации на объекте.

ConcurrentHashMap

  • Структура карты безопасна для параллельного чтения и записи разных ключей (lock-free на уровне бакетов).
  • Race condition возможен только на изменяемых значениях, если вы просто храните объекты без атомарных операций.
  • Решение: использовать AtomicInteger, AtomicReference или методы типа compute, computeIfAbsent.

ConcurrentSkipListMap / ConcurrentSkipListSet

  • Структура корректна, операции lock-free через CAS.
  • Параллельные изменения одного ключа не атомарны на уровне значения — нужна атомика.
  • Подходит, если нужен отсортированный потокобезопасный набор/карта.

AtomicInteger / AtomicLong / AtomicReference

  • Полностью атомарные операции, lock-free.
  • Нет риска потерять обновления, даже если несколько потоков одновременно меняют значение.
  • Часто используются вместе с ConcurrentHashMap для безопасного обновления значений.

🔹 Практическое правило

  • Если коллекция не потокобезопасная → либо используйте внешнюю синхронизацию, либо замените на потокобезопасную версию (ConcurrentHashMap, ConcurrentSkipListMap).
  • Если храните изменяемые объекты внутри карты → используйте атомики или compute-методы, иначе значения могут теряться.
  • Если нужна максимальная параллельность → ConcurrentHashMap + AtomicInteger/Reference лучший вариант.
  • Если нужна простая синхронизация без заморачиваний → Collections.synchronizedMap/Set — но помните про блокировку всей коллекции.

Всего лайков: 1
Мой канал в социальных сетях
Отправляя email, вы принимаете условия политики конфиденциальности

Полезные статьи:

От микросервисной революции к эпохе эффективности
Период 2010–2020 годов можно назвать эпохой разделения и масштабирования. Системы стали слишком большими, чтобы оставаться монолитами. Решением стали микросервисы — маленькие автономные приложения, ра...
Типы данных в Java
Типы данных в Java Привет! С вами Виталий Лесных. В этом уроке курса «Основы Java для начинающих» разберем, что такое типы данных. Типы данных — это фундамент любого языка программирования. С их помо...
Как удержать легаси-проект от смерти и подарить ему ещё 10 лет
Признаки легаси-проекта: как распознать старый корабль Легаси — это не просто старый код. Это живой организм, который пережил десятки изменений, смену команд, устаревшие технологии и множество временн...

Новые статьи:

Java под микроскопом: стек, куча и GC на примере кода
Схема - Java Memory Model - Heap / Non-Heap / Stack Heap (память для объектов) Создаёт объекты через new. Young Generation: Eden + Survivor. Old Generation: объекты, пережившие несколько сборок G...
Как удержать легаси-проект от смерти и подарить ему ещё 10 лет
Признаки легаси-проекта: как распознать старый корабль Легаси — это не просто старый код. Это живой организм, который пережил десятки изменений, смену команд, устаревшие технологии и множество временн...
Асинхронность и реактивность в Java: CompletableFuture, Flow и Virtual Threads
В современном Java-разработке есть три основных подхода к асинхронности и параллельности: CompletableFuture — для одиночных асинхронных задач. Flow / Reactive Streams — для потоков данных с контролем...
Fullscreen image