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

Когда использовать synchronized collections?

Представьте туалет с одним ключом — пока один поток внутри, все остальные ждут в очереди, даже если просто хотят посмотреть, свободен ли он.

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

🟢 Junior Level

Synchronized collections — обычные коллекции с блокировкой на каждый метод.

Представьте туалет с одним ключом — пока один поток внутри, все остальные ждут в очереди, даже если просто хотят посмотреть, свободен ли он.

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());

Проблема: Только ОДИН поток может работать одновременно (даже читать!).

Современная альтернатива:

// ❌ Старый подход (медленный)
List<String> list = Collections.synchronizedList(new ArrayList<>());

// ✅ Новый подход (быстрый)
Map<String, Integer> map = new ConcurrentHashMap<>();
List<String> list = new CopyOnWriteArrayList<>();

synchronizedList — один lock на ВСЕ операции, включая чтение. ConcurrentHashMap — lock-free чтение и гранулярные локи на запись, поэтому N потоков читают параллельно.


🟡 Middle Level

Проблема: один лок на всё

// 100 потоков хотят прочитать
syncList.get(0);  // Все ждут в очереди!
// → 0 параллелизма

Составные операции НЕ атомарны

// ❌ Race condition!
if (!syncList.contains(item)) {  // Поток A проверил
    // Поток B вклинился и добавил
    syncList.add(item);  // Поток A добавил дубликат!
}

// ✅ Внешний synchronized
synchronized (syncList) {
    if (!syncList.contains(item)) {
        syncList.add(item);
    }
}

Итерация требует ручного лока

// ❌ ConcurrentModificationException!
for (String s : syncList) { ... }

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

Когда использовать

Сценарий Что выбрать
Новый код ConcurrentHashMap
Списки слушателей CopyOnWriteArrayList
Полная блокировка synchronizedList
Legacy код synchronizedList

COWAL хорош для слушателей: итерация (обход всех listeners) чаще, чем добавление listener. COWAL даёт CME-free итерацию и O(1) добавление.


🔴 Senior Level

Когда НЕ использовать synchronized collections

  1. Высокая конкуренция (>10 потоков) — ConcurrentHashMap даст на порядок лучший throughput
  2. Частые итерации — каждая требует ручного synchronized(list) { ... }
  3. Составные операции (check-then-act) — всё равно нужен внешний synchronized, так зачем обёртка?

Coarse-grained locking

Один монитор на ВСЮ коллекцию:
  → 100 читателей → сериализация
  → На многоядерных CPU → 0 масштабируемости
  
ConcurrentHashMap:
  → Lock-free чтение
  → CAS + synchronized на корзину
  → 100 потоков работают параллельно

Double synchronization

// ❌ Двойная блокировка!
synchronized (syncList) {
    syncList.add(e);  // Внутренний synchronized + внешний
}

// synchronizedList уже синхронизирует каждый метод
// → Лишний overhead

Экспорт оригинальной коллекции

// ❌ Нарушение thread-safety!
List<String> original = new ArrayList<>();
List<String> sync = Collections.synchronizedList(original);

// Изменение в обход обёртки:
original.add("x");  // НЕ синхронизировано!

Когда synchronized collections оправданы

// 1. Legacy системы (код до Java 5)

// 2. Грубая атомарность всей коллекции
synchronized (syncList) {
    // Гарантированно никто не вклинится
    for (var item : syncList) {
        process(item);
    }
}

// 3. Низкая конкуренция + малый размер
// → Обертка дешевле ConcurrentHashMap

Production Experience

Реальный сценарий: synchronizedMap убил throughput

  • API: 1000 RPS, synchronizedMap для кэша
  • Lock contention: 90% времени на synchronized
  • Решение: ConcurrentHashMap
  • Результат: +900% throughput

Best Practices

  1. НЕ используйте в новом коде
  2. ConcurrentHashMap — по умолчанию
  3. CopyOnWriteArrayList — для listeners
  4. Итерация → synchronized блок обязателен
  5. Составные операции → внешний synchronized
  6. Не экспортируйте оригинальную коллекцию
  7. Vector/Hashtable → избегайте в новом коде. Единственный случай, когда терпимы — single-threaded legacy-код, где замена не оправдана рисками.

Резюме для Senior

  • Один лок → 0 масштабируемости
  • Составные операции → race condition без внешнего synchronized
  • Итерация → ручной synchronized блок
  • ConcurrentHashMap > synchronizedMap в 99% случаев
  • Legacy → единственное оправдание
  • Vector/Hashtable → встроенные synchronized → legacy

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

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

  • synchronized collections — одна блокировка на все методы (чтение + запись)
  • Составные операции (check-then-act) НЕ атомарны — нужен внешний synchronized
  • Итерация требует ручного synchronized(list) { ... } блока
  • ConcurrentHashMap даёт lock-free чтение и гранулярные локи на запись
  • CopyOnWriteArrayList идеален для списков слушателей (частое чтение, редкая запись)
  • В новом коде synchronized collections не рекомендуются
  • Экспорт оригинальной коллекции нарушает thread-safety
  • Vector/Hashtable — legacy, избегать в новом коде

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

  • Почему synchronizedMap убивает throughput при 1000 RPS? — 90% времени тратится на lock contention; ConcurrentHashMap решает проблему.
  • Атомарна ли конструкция if (!list.contains(x)) list.add(x)? — Нет, это два отдельных вызова; нужен внешний synchronized(list).
  • Почему итерация по synchronizedList без synchronized блока выбросит CME? — Итерация — составная операция (hasNext + next), не защищена одним вызовом метода.
  • Когда synchronized collections оправданы? — Legacy код до Java 5, низкая конкуренция, грубая атомарность всей коллекции.

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

  • «synchronizedList — хороший выбор для нового кода» — нет, ConcurrentHashMap/CopyOnWriteArrayList лучше
  • «Составные операции атомарны в synchronizedList» — нет, нужны внешние synchronized блоки
  • «Vector — нормальный выбор» — это legacy класс с грубой блокировкой
  • «synchronizedList решает все проблемы конкурентности» — не решает race condition для составных операций

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

  • [[22. Как получить synchronized коллекцию]]
  • [[26. Что такое fail-fast и fail-safe итераторы]]
  • [[27. Что такое ConcurrentModificationException]]