Как устроен итератор ConcurrentHashMap так, что он допускает параллельные изменения карты без ConcurrentModificationException?
Итератор ConcurrentHashMap является слабо согласованным: он не фиксирует неизменяемый снимок карты и не использует fail-fast-проверку структурных изменений. Поэтому параллельные добавления и удаления не приводят к ConcurrentModificationException, но обход может увидеть часть изменений, произошедших после его создания.
Обычные коллекции вроде HashMap не предназначены для конкурентного доступа, а fail-fast-итераторы таких коллекций помогают обнаруживать ошибочное структурное изменение во время обхода. Однако для высококонкурентных сценариев немедленное завершение итерации при каждом изменении было бы слишком жёстким ограничением.
ConcurrentHashMap создавался для случаев, когда чтения, записи и обходы должны продолжаться одновременно с минимальной блокировкой. Поэтому его итератор ориентирован не на строгий снимок, а на безопасное продолжение обхода при изменениях.
Представим мониторинг карты, где одни потоки обновляют состояния объектов, а другой поток периодически обходит записи. Требование может состоять в том, чтобы обход не падал из-за обычных конкурентных обновлений.
Если использовать итератор с fail-fast-поведением, обход может завершиться исключением. Если требовать полного снимка, придётся дополнительно платить за копирование или синхронизацию, а данные всё равно могут устареть сразу после создания снимка.
Итератор ConcurrentHashMap не захватывает глобальную блокировку на всю карту и не проверяет модификации по счётчику, как это обычно делают fail-fast-реализации. Он проходит по внутренним структурам таблицы, которые изменяются конкурентными операциями с использованием механизмов видимости и атомарности, применяемых самой картой.
Такой итератор гарантирует безопасное выполнение обхода: он не должен выбрасывать ConcurrentModificationException из-за параллельных изменений. При этом результат не является консистентным снимком на один момент времени: добавленная запись может попасть или не попасть в обход, а удалённая запись может быть замечена в зависимости от момента и участка таблицы, который уже обрабатывается.
Итератор не предназначен для логики, требующей точного набора записей «на начало обхода». Для такой задачи нужен явный снимок, например отдельная копия данных, либо внешняя синхронизация. Цена слабой согласованности — отсутствие единой временной точки, зато обход не блокирует всю карту и сохраняет пригодность для конкурентного чтения.
Добавление записи во время обхода не обязано приводить к исключению, но также не гарантируется, что ключ 2 будет обработан этим же итератором. Если требуется детерминированный набор записей, следует заранее создать копию.
В сервисе выполнялся периодический обход ConcurrentHashMap с состояниями заказов. Параллельные потоки добавляли новые заказы и удаляли завершённые. Требовалось формировать приблизительную статистику без остановки обработки заказов.
Рассматривались три варианта. HashMap с ручной синхронизацией мог обеспечить строгий контроль, но блокировал доступ и усложнял код. Копирование карты перед каждым обходом давало снимок, но создавало дополнительную нагрузку по памяти и времени. ConcurrentHashMap позволял продолжать обновления и обход одновременно, но статистика могла не включать самые свежие изменения.
Выбрали ConcurrentHashMap, потому что статистика была периодической и допускала небольшую временную погрешность. Для операций, где требовался точный согласованный набор данных, отдельно формировали снимок и явно фиксировали момент его создания.
Нет, это не следует трактовать как гарантию строгого снимка. Итератор предназначен для слабой согласованности: он безопасно работает при изменениях, но его результат не обладает полной атомарностью относительно всех операций карты. Нельзя использовать такой обход как доказательство того, что обработана ровно вся карта в определённый момент.
Нет. Отсутствие исключения означает лишь, что сам итератор допускает конкурентные изменения. Логика обработки может оставаться некорректной: запись способна измениться сразу после чтения, а последовательность «прочитать — проверить — обновить» не становится атомарной автоматически. Для составных действий нужны атомарные методы карты, такие как compute, merge или putIfAbsent, либо другая координация потоков.
Итератор CopyOnWriteArrayList работает с массивом, который существовал на момент создания итератора, поэтому последующие изменения списка он не видит. Итератор ConcurrentHashMap не использует такой неизменяемый снимок: он может увидеть некоторые изменения, произошедшие после его создания. Оба подхода избегают обычного fail-fast-сценария, но компромиссы различны: CopyOnWriteArrayList дорог по записи и удобен для частого чтения, а ConcurrentHashMap допускает конкурентные изменения самой карты и предоставляет менее строгую картину обхода.