Вопрос 22 · Раздел 4

Как получить synchronized коллекцию?

4. Поток B получает доступ

Версии по языкам: English Russian Ukrainian

Synchronized коллекция — это обёртка (decorator) вокруг обычной коллекции, которая оборачивает каждый метод в synchronized-блок. Это гарантирует, что одновременно только один поток может вызвать любой метод коллекции.

🟢 Junior Level

Способ через Collections.synchronizedX:

List<String> list = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
Set<String> set = Collections.synchronizedSet(new HashSet<>());

ВАЖНО: итерация требует synchronized!

// ❌ Неправильно
for (String s : syncList) { ... }

// ✅ Правильно
synchronized (syncList) {
    for (String s : syncList) { ... }
}

🟡 Middle Level

Фабричные методы

Collections.synchronizedList(list)
Collections.synchronizedMap(map)
Collections.synchronizedSet(set)
Collections.synchronizedNavigableMap(navigableMap)  // Java 8+

Механика: Decorator паттерн

// Внутри:
class SynchronizedList<E> implements List<E> {
    final List<E> list;
    final Object mutex;  // = this по умолчанию

    public synchronized E get(int index) {
        return list.get(index);  // synchronized на mutex
    }
}

Что происходит при конкурентном доступе:

  1. Поток A вызывает get(0) → захватывает монитор mutex
  2. Поток B вызывает get(1) → блокируется на synchronized, ждёт
  3. Поток A завершает → освобождает монитор
  4. Поток B получает доступ

Сравнение с альтернативами

Способ Когда
Collections.synchronizedX Полная блокировка нужна
ConcurrentHashMap Highload, частые записи
CopyOnWriteArrayList Списки слушателей
Vector/Hashtable НИКОГДА (legacy)

Типичные ошибки

// 1. Забытый synchronized при итерации
for (var e : syncList) { ... }  // CME!

// 2. Экспорт оригинала
List original = new ArrayList<>();
List sync = Collections.synchronizedList(original);
original.add("x");  // Thread-safety нарушена!

// 3. Двойная синхронизация
synchronized (syncList) {
    syncList.add(e);  // Внутренний + внешний synchronized
}

🔴 Senior Level

Mutex object

// По умолчанию mutex (mutual exclusion — объект-ключ для synchronized) = this (обёртка)
// → Все synchronized методы блокируют друг друга
// backing collection — оригинальная коллекция, которую оборачивает synchronized-обёртка

// Можно (через рефлексию) установить свой mutex
// → Но JDK API не поддерживает напрямую

Production Experience

Почему ConcurrentHashMap лучше:

synchronizedMap:
  → 1 лок на всё
  → Чтение блокирует чтение
  
ConcurrentHashMap:
  → Lock-free чтение
  → CAS для записи
  → Гранулярные локи на корзину

Best Practices

  1. Итерация → synchronized блок
  2. Не экспортируйте оригинал
  3. ConcurrentHashMap предпочтительнее
  4. mutex = this по умолчанию
  5. Избегайте двойной синхронизации

Резюме для Senior

  • Decorator паттерн → synchronized на каждый метод
  • Итерация → ручной synchronized блок
  • mutex = this → полная блокировка
  • ConcurrentHashMap > synchronizedMap
  • Не экспортируйте backing collection (оригинал, который оборачивается)

🎯 Шпаргалка для интервью

Обязательно знать:

  • Collections.synchronizedList/Set/Map — фабричные методы-обёртки (Decorator паттерн)
  • Каждая синхронизируется на одном mutex объекте (по умолчанию this)
  • Итерация обязательно требует synchronized(list) { ... } блока
  • Оригинальная коллекция остаётся доступной — изменение через неё нарушает thread-safety
  • Двойная синхронизация (synchronized(syncList) { syncList.add(e); }) — лишний overhead
  • Альтернативы: ConcurrentHashMap (highload), CopyOnWriteArrayList (списки слушателей)
  • Vector/Hashtable — legacy, НИКОГДА в новом коде

Частые уточняющие вопросы:

  • Что такое mutex в synchronized коллекциях? — Объект-ключ для synchronized; по умолчанию this, все методы блокируют друг друга.
  • Почему итерация по synchronizedList без synchronized блока приводит к CME? — Итератор вызывает hasNext/next отдельно; между ними другой поток может изменить коллекцию.
  • Чем Collections.synchronizedList отличается от ConcurrentHashMap? — Первый — один лок на всё; второй — lock-free чтение + гранулярные локи.
  • Что такое backing collection? — Оригинальная коллекция, которую оборачивает synchronized-обёртка; доступ к ней в обход обёртки ломает thread-safety.

Красные флаги (НЕ говорить):

  • «Можно менять оригинальную коллекцию без последствий» — это нарушает thread-safety
  • «synchronizedList гарантирует атомарность составных операций» — нет, нужен внешний synchronized
  • «Iterator.remove() работает без synchronized для synchronizedList» — итерация всегда требует ручной блокировки
  • «Vector — современная альтернатива» — это legacy с грубой блокировкой

Связанные темы:

  • [[21. Когда использовать synchronized collections]]
  • [[23. Что такое Collections.unmodifiableList()]]
  • [[25. В чём разница между Iterator и ListIterator]]