Когда использовать synchronized collections
Также к этой категории относятся устаревшие классы из Java 1.0 — Vector и Hashtable, в которых ключевое слово synchronized добавлено прямо в сигнатуры методов.
🟢 Junior Level
1. Что такое синхронизированные коллекции
Синхронизированные коллекции (Synchronized Collections) — это обёртки вокруг стандартных коллекций (ArrayList, HashSet, HashMap), в которых каждый метод защищен грубой блокировкой на внутреннем мьютексе (synchronized (mutex)):
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
Также к этой категории относятся устаревшие классы из Java 1.0 — Vector и Hashtable, в которых ключевое слово synchronized добавлено прямо в сигнатуры методов.
2. Главная проблема: сериализация доступа
В синхронизированных коллекциях используется один общий монитор на абсолютно все операции:
- Если Поток 1 выполняет чтение
get(0), Поток 2 не может даже прочитатьget(1)и встает в очередь ожидания. - На современных многоядерных процессорах это полностью уничтожает параллелизм (пропускная способность падает до скорости одного ядра).
В современном коде Java для многопоточной разработки по умолчанию используются структуры из пакета java.util.concurrent:
// ❌ УСТАРЕЛО (грубая блокировка на чтение и запись):
Map<String, User> map = Collections.synchronizedMap(new HashMap<>());
// ✅ СОВРЕМЕННЫЙ СТАНДАРТ (Lock-Free чтение + гранулярные блокировки):
Map<String, User> map = new ConcurrentHashMap<>();
🟡 Middle Level
1. Ловушка составных операций (Compound Actions)
Потокобезопасность отдельных методов не гарантирует потокобезопасности их комбинации:
// ❌ КРИТИЧЕСКАЯ ОШИБКА: Race Condition при Check-Then-Act!
if (!syncList.contains(user)) {
// В этот момент поток вытесняется планировщиком!
// Другой поток успевает проверить и добавить user.
syncList.add(user); // Дубликат добавлен!
}
// ✅ Единственно верное исправление: внешний блок синхронизации
synchronized (syncList) {
if (!syncList.contains(user)) {
syncList.add(user);
}
}
Методы contains() и add() по отдельности потокобезопасны, но замок освобождается между их вызовами.
2. Обязательная ручная синхронизация при итерации
Метод syncList.iterator() возвращает обычный итератор базовой коллекции. Так как обход коллекции состоит из последовательности вызовов hasNext() и next(), блокировка на время всей итерации автоматически не удерживается:
// ❌ ПРИВЕДЕТ К ConcurrentModificationException В ПРОДЕ:
for (String item : syncList) {
process(item);
}
// ✅ ТРЕБОВАНИЕ СПЕЦИФИКАЦИИ JDK:
synchronized (syncList) {
for (String item : syncList) {
process(item);
}
}
Если другой поток вызовет syncList.add() во время итерации без внешнего synchronized-блока, первый поток упадет с ошибкой ConcurrentModificationException.
🔴 Senior Level
1. Когда синхронизированные коллекции ВСЁ ЕЩЕ оправданы
Несмотря на доминирование java.util.concurrent, существуют конкретные инженерные сценарии, когда использование Collections.synchronized* является оптимальным выбором:
- Необходимость монопольной грубой блокировки всей структуры (Coarse-Grained Atomicity):
Если приложению необходимо атомарно выполнить серию сложных преобразований над всем списком (например, очистить, заполнить новыми данными и отсортировать), наличие одного разделяемого мьютекса упрощает архитектуру:
synchronized (syncList) { syncList.clear(); syncList.addAll(newBatch); Collections.sort(syncList); }В
ConcurrentHashMapилиCopyOnWriteArrayListатомарно заблокировать всю структуру «снаружи» для пакетного обновления невозможно без введения дополнительных внешних замков. -
Отсутствие аналогов в
java.util.concurrent: Вjava.util.concurrentнет прямого аналога масштабируемого связанного списка с произвольным доступом по индексу (ConcurrentArrayList). Если в приложении преобладают частые операции записи и чтения по индексу,CopyOnWriteArrayListнеприменим ($O(N)$ на запись), аCollections.synchronizedList(new ArrayList<>())остается единственным стандартным решением из JDK. - Интеграция с устаревшими API (Legacy Libraries):
Библиотеки, ожидающие классический
ListилиMap, где требуется гарантировать взаимное исключение потоков без изменения контрактов интерфейсов.
2. Скрытая утечка ссылки (Aliasing / Reference Leak)
Фатальная ошибка при создании синхронизированной коллекции — сохранение и использование ссылки на исходный объект:
List<String> rawList = new ArrayList<>();
List<String> syncList = Collections.synchronizedList(rawList);
// Поток 1 работает через обертку:
syncList.add("one");
// Поток 2 случайно получил ссылку на исходный rawList:
rawList.add("two"); // КАТАСТРОФА: модификация в обход блокировок!
Прямая модификация rawList разрушит внутреннее состояние коллекции и приведет к гонкам данных (Data Races).
[!IMPORTANT] При создании синхронизированных обёрток никогда не сохраняйте ссылку на исходный экземпляр:
List<String> list = Collections.synchronizedList(new ArrayList<>());
3. Сравнение архитектурных альтернатив
| Сценарий применения | Рекомендуемая коллекция | Почему не synchronized*? |
|---|---|---|
| Ассоциативный массив (Map) | ConcurrentHashMap |
Lock-Free чтение, в сотни раз выше throughput |
| Множество (Set) | ConcurrentHashMap.newKeySet() |
Масштабируемость без единого мьютекса |
| Список с частым чтением (Listeners) | CopyOnWriteArrayList |
Чтение без блокировок, snapshot-итераторы |
| Очередь задач (Producer-Consumer) | BlockingQueue / ConcurrentLinkedQueue |
Встроенное ожидание, отсутствие взаимных блокировок |
| Список с частыми записями и чтениями | Collections.synchronizedList |
COWAL дает $O(N)$ на вставку, альтернатив в JDK нет |
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«Синхронизированные коллекции (
Collections.synchronizedList/Map/Set) оборачивают базовые коллекции, синхронизируя каждый метод через единый мьютекс. Они практически вытеснены классами пакетаjava.util.concurrent(ConcurrentHashMap,CopyOnWriteArrayList), поскольку создают общее бутылочное горлышко даже для параллельных чтений. Основные сферы применения сегодня: редкие сценарии со списком с частой записью по индексу (гдеCOWALнеприменим из-за копирования), пакетные атомарные операции над всей коллекцией под одним замком и совместимость со старыми библиотеками».
3 каверзных вопроса на интервью с ответами
1. Почему вызов for (String s : Collections.synchronizedList(list)) выбрасывает ConcurrentModificationException, если список синхронизирован?
Ответ: Цикл for-each транслируется компилятором в вызовы Iterator.hasNext() и Iterator.next(). Метод syncList.iterator() потокобезопасно возвращает итератор, но сам монитор освобождается сразу после создания итератора. Если между шагами цикла параллельный поток изменит список, внутренний счетчик модификаций modCount не совпадет с expectedModCount, и итератор выбросит ConcurrentModificationException. Обход синхронизированной коллекции обязан оборачиваться во внешний блок synchronized(syncList).
2. В чём разница между Vector и Collections.synchronizedList(new ArrayList<>())?
Ответ:
Vectorсинхронизирует методы на собственном экземпляре (this), в то время какsynchronizedListсинхронизируется на выделенном объектеmutex(позволяя передавать собственный мьютекс в конструкторе).Vectorувеличивает массив при переполнении в 2 раза (или на величинуcapacityIncrement), аArrayListвнутри обёртки увеличивается в 1.5 раза.synchronizedListреализует паттерн «Адаптер/Декоратор» над любой существующей реализациейList(например,LinkedList).
3. Атомарна ли операция инкремента счетчика в Collections.synchronizedMap: map.put(key, map.get(key) + 1)?
Ответ: Категорически нет. Это три независимых действия: чтение (get), инкремент числа в регистре процессора и запись (put). Замок мапы захватывается и освобождается дважды, между ними любой поток может изменить значение по этому ключу. Для обеспечения атомарности требуется внешний блок synchronized (map) { ... } либо переход на ConcurrentHashMap.merge() / compute().
Типичные ошибки и Red Flags
- ❌ Использование
Collections.synchronizedMapв новых высоконагруженных проектах: Создает жесткий Lock Contention, необходимо использоватьConcurrentHashMap. - ❌ Итерация по синхронизированной коллекции без блока
synchronized(collection): Гарантированный источникConcurrentModificationExceptionв многопоточной среде. - ❌ Уверенность в атомарности проверок
if (!list.contains(e)) list.add(e): Ошибка проектирования Check-Then-Act. - ❌ Утечка ссылки на обернутую коллекцию: Модификация через прямую ссылку ломает потокобезопасность обертки.