Программирование JavaКоллекцииJava-разработчик серверных приложений

При обходе коллекции, обёрнутой в Collections.synchronizedMap, почему синхронизации вызова iterator недоста...

При обходе коллекции, обёрнутой в Collections.synchronizedMap, почему синхронизации вызова iterator() недостаточно для безопасной итерации?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Collections.synchronizedMap синхронизирует отдельные методы обёрнутой карты, но возвращённый итератор не удерживает этот монитор на протяжении всего обхода. Поэтому итерацию нужно выполнять внутри блока, синхронизированного на той же обёртке; иначе другой поток может изменить карту во время обхода, что приведёт к гонке и, для некоторых реализаций, к ConcurrentModificationException.

Исторический контекст

Синхронизированные обёртки появились как способ добавить базовую взаимную блокировку к обычным коллекциям без создания отдельной потокобезопасной реализации. Такой подход защищает операции вроде get, put или remove, выполняя их под общим монитором.

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

Постановка проблемы

Если один поток обходит карту, а другой одновременно изменяет её, итератор может наблюдать несогласованное состояние коллекции. Для HashMap это может проявиться как ConcurrentModificationException, но fail-fast-поведение является диагностическим и не заменяет синхронизацию.

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

Подробное решение

Итерировать нужно под монитором самой синхронизированной обёртки, потому что именно её методы используют для блокировки. Блокировка исходной карты или отдельного объекта не защищает операции обёртки от параллельного доступа через её публичный интерфейс.

import java.util.Collections; import java.util.HashMap; import java.util.Map; Map<String, Integer> scores = Collections.synchronizedMap(new HashMap<>()); synchronized (scores) { for (Map.Entry<String, Integer> entry : scores.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } }

Блокировка должна охватывать весь жизненный цикл итератора — от начала обхода до последнего вызова next(). Объект представления entrySet() сам по себе не является заменой этой внешней синхронизации.

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

Ситуация из практики

Сервис хранил настройки пользователей в synchronizedMap. Поток обновления периодически изменял настройки, а поток аудита обходил entrySet() и записывал содержимое в журнал. Несмотря на синхронизированные методы карты, аудит иногда получал ConcurrentModificationException.

Рассматривались три варианта. Игнорировать исключение было неправильно: это скрывало гонку. Использовать ConcurrentHashMap можно было, но обход там имеет слабосогласованную семантику и не гарантирует единый снимок. В итоге для короткого аудита выбрали внешний synchronized на обёртке и удерживали блокировку до завершения обхода.

Это обеспечило согласованный проход, но увеличило время ожидания обновлений. Для длинных отчётов применили другой вариант: под блокировкой быстро скопировали записи в отдельный список, отпустили монитор и сформировали отчёт уже по снимку.

Что кандидаты часто упускают

  1. Достаточно ли синхронизировать только создание итератора?

Нет. После создания итератор продолжает обращаться к внутреннему состоянию коллекции при каждом переходе к следующему элементу. Поэтому монитор должен удерживаться на всём протяжении обхода, а не только вокруг вызова iterator().

  1. Почему нельзя синхронизироваться на исходной HashMap, переданной в обёртку?

Потому что synchronizedMap обычно использует в качестве монитора сам объект обёртки. Блокировка исходной карты и блокировка обёртки — разные механизмы, поэтому поток, работающий через обёртку, не обязан ждать блокировку исходной карты. Надёжное правило — не раскрывать исходную коллекцию и синхронизироваться на объекте, который используется как потокобезопасный интерфейс.

  1. Гарантирует ли ConcurrentHashMap согласованный снимок при итерации?

Нет. Её итераторы не выбрасывают ConcurrentModificationException из-за обычных конкурентных изменений и могут отражать некоторые изменения, произошедшие после создания итератора. Это подходит для безопасного обхода живой структуры, но не для требования «увидеть карту в одном фиксированном состоянии»; для снимка нужна отдельная стратегия копирования или другая координация потоков.