Можно ли считать итерацию по ConcurrentHashMap точным снимком её содержимого?
Нет. Итератор ConcurrentHashMap является слабо согласованным: он не фиксирует атомарный снимок карты, может отражать часть изменений, выполненных параллельно с обходом, и не обязан показывать единое состояние карты в один момент времени. При этом он не выбрасывает ConcurrentModificationException из-за конкурентных изменений.
Обычные коллекции Java не предназначены для безопасной структурной модификации во время обхода. Обёртки вроде синхронизированных коллекций защищают отдельные операции, но для согласованной итерации требуют внешней синхронизации, что ограничивает параллелизм.
ConcurrentHashMap, появившаяся в пакете java.util.concurrent начиная с Java 5, решает другую задачу: позволяет нескольким потокам одновременно читать и изменять карту без блокировки всей коллекции на каждую операцию. Цена этого подхода — отсутствие гарантии атомарного снимка при обходе.
Предположим, поток формирует отчёт, обходя карту, пока другие потоки добавляют, заменяют или удаляют записи. Если разработчик ошибочно воспринимает итерацию как снимок, отчёт может содержать смесь состояний: часть новых записей, часть старых, а некоторые изменения не будут замечены.
Такая итерация подходит для статистики, мониторинга и фоновой обработки, где допустима приблизительная картина. Она не подходит для проверки глобального инварианта или формирования юридически значимого отчёта, требующего единого состояния данных.
Итераторы ConcurrentHashMap гарантируют безопасный обход без блокировки всей карты и не падают из-за конкурентных структурных изменений. Они слабо согласованы: элементы, добавленные или удалённые во время обхода, могут быть соответственно увидены или не увидены, а наблюдаемое содержимое не обязано соответствовать состоянию карты в конкретный момент времени.
Результат обхода недетерминирован: поток может увидеть старую запись, новую запись, обе записи или только одну из них. Сам обход при этом остаётся безопасным с точки зрения коллекции.
Если нужен настоящий согласованный снимок, его следует создавать в момент, когда изменения координируются: например, остановить писателей, использовать общий механизм блокировки для чтения и записи или заменить состояние неизменяемым объектом под одной атомарной ссылкой. Простое копирование из ConcurrentHashMap во время конкурентных изменений само по себе не превращает копирование в атомарный снимок.
Внешняя блокировка помогает только тогда, когда все операции записи и чтения, участвующие в протоколе, используют ту же блокировку. Если часть кода продолжает напрямую изменять карту, согласованность не обеспечивается.
Сервис хранит активные подключения в ConcurrentHashMap, а отдельный поток раз в минуту собирает диагностический отчёт. Рассматривались три варианта.
Первый — обходить карту напрямую. Это почти не мешает рабочим потокам и достаточно для приблизительной телеметрии, но отчёт может быть внутренне неоднородным. Второй — синхронизировать каждую операцию и весь обход общей блокировкой. Это даёт согласованный результат, но длинный отчёт временно останавливает изменения карты.
Третий — поддерживать отдельное неизменяемое состояние и атомарно публиковать его после построения. Читатели получают согласованный снимок без блокировки, но обновление требует дополнительной памяти и затрат на копирование.
Для диагностического отчёта выбран первый вариант: бизнес-решения по его данным не принимаются, а минимальное влияние на рабочий трафик важнее строгой согласованности. Для расчёта платежей применили бы второй или третий вариант, поскольку приблизительный снимок там недопустим.
Нет, это не является его контрактом. Итератор специально рассчитан на конкурентный доступ и является слабо согласованным, поэтому структурные изменения не требуют остановки обхода и не приводят к обязательному ConcurrentModificationException. Это не означает, что результат обхода точен или атомарен.
Нельзя использовать итератор как гарантию полного набора исходных записей при параллельных изменениях. Он может увидеть многие элементы, присутствовавшие в начале обхода, но конкурентные удаления, внутреннее продвижение итератора и изменения карты не дают гарантии единого начального состояния. Для полного и воспроизводимого набора нужен явно созданный согласованный снимок.
Нет, значение size() при конкурентных изменениях отражает лишь наблюдение в определённый момент и может устареть сразу после чтения. Даже если размер равен нужному порогу, другой поток может изменить карту до выполнения действия, поэтому проверка размера не образует атомарной проверки с последующим решением. Для такого протокола нужна атомарная операция над отдельным состоянием, соответствующая бизнес-инварианту, например условное обновление через compute, merge или атомарный примитив.