Оглавление:
- 1️⃣ HashMap / TreeMap / TreeSet (не потокобезопасные)
- 2️⃣ SynchronizedMap / SynchronizedSortedMap / SynchronizedSortedSet
- 3️⃣ ConcurrentHashMap
- 4️⃣ ConcurrentSkipListMap / ConcurrentSkipListSet
- 5️⃣ AtomicInteger / AtomicReference / AtomicLong
- Таблиц для сравнения: один элемент, два потока
- Таблица: один элемент редактируется двумя потоками + новый ключ добавляется
- Сравнение потокобезопасных отсортированных коллекций
- 🔹 Выводы
- 🔹 Практическое правило
Понимаем многопоточность в Java через коллекции и атомики
1️⃣ HashMap / TreeMap / TreeSet (не потокобезопасные)
HashMap:- Структура: массив бакетов + связные списки / деревья (для коллизий).
- Под капотом: при put/remove происходит модификация массива бакетов и, возможно, переупорядочивание цепочек.
- Проблема при многопоточности: два потока могут одновременно менять один бакет → corrupt структуры, потеря элементов, бесконечный цикл при итерации.
- Структура: красно-черное дерево.
- При вставке/удалении поток изменяет несколько узлов дерева.
- Одновременные изменения → структура дерева может сломаться, 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 | |
❌ race condition, одно обновление может потеряться | ❌ добавление нового ключа может сломать структуру | Нет синхронизации, все потоки одновременно меняют бакеты | Высокий риск corrupt |
| TreeMap / TreeSet | |
❌ race condition, структура дерева может сломаться | ❌ добавление нового ключа меняет узлы дерева | Нет потокобезопасности, балансировка дерева ломается | Высокий риск corrupt |
| SynchronizedMap / SynchronizedSortedMap / SynchronizedSortedSet | |
✅ Поток 1 и 2 выполняются последовательно (блокировка на мониторе) | ✅ Поток 3 ждёт завершения | Все методы синхронизированы на объекте, итерация требует блока | Низкий, безопасно |
| ConcurrentHashMap | |
⚠️ Race condition на значении, структура карты корректна | ✅ Добавление нового ключа параллельно | Lock-free бакеты + CAS, чтение lock-free | Значение может потеряться, структура в порядке |
| ConcurrentSkipListMap / Set | |
⚠️ Race condition на значении | ✅ Добавление нового ключа параллельно | Skip list + CAS, lock-free | Значение может потеряться, структура в порядке |
| AtomicInteger / AtomicReference | |
✅ атомарно, оба потока корректно обновляют значение | ✅ добавление нового ключа через отдельный объект 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— но помните про блокировку всей коллекции.
Мой канал в социальных сетях
Полезные статьи:
Период 2010–2020 годов можно назвать эпохой разделения и масштабирования. Системы стали слишком большими, чтобы оставаться монолитами. Решением стали микросервисы — маленькие автономные приложения, ра...
Типы данных в Java Привет! С вами Виталий Лесных. В этом уроке курса «Основы Java для начинающих» разберем, что такое типы данных. Типы данных — это фундамент любого языка программирования. С их помо...
Признаки легаси-проекта: как распознать старый корабль Легаси — это не просто старый код. Это живой организм, который пережил десятки изменений, смену команд, устаревшие технологии и множество временн...
Новые статьи:
Схема - Java Memory Model - Heap / Non-Heap / Stack Heap (память для объектов) Создаёт объекты через new. Young Generation: Eden + Survivor. Old Generation: объекты, пережившие несколько сборок G...
Признаки легаси-проекта: как распознать старый корабль Легаси — это не просто старый код. Это живой организм, который пережил десятки изменений, смену команд, устаревшие технологии и множество временн...
В современном Java-разработке есть три основных подхода к асинхронности и параллельности: CompletableFuture — для одиночных асинхронных задач. Flow / Reactive Streams — для потоков данных с контролем...