Коли використовувати 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)і зупиняється в черзі очікування. - На сучасних багатоядерних процесорах це повністю вбиває паралелізм (пропускна здатність деградує до швидкості одного ядра CPU).
У сучасному промисловому коді 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() або remove() під час ітерації без зовнішнього блоку 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: У JDK немає конкурентного динамічного масиву з довільним доступом за індексом (ConcurrentArrayList). Якщо в системі переважають часті записи та читання за індексом,CopyOnWriteArrayListнепридатний через $O(N)$ копіювання на кожен запис, аCollections.synchronizedList(new ArrayList<>())залишається єдиним стандартним рішенням зі стандартної бібліотеки. - Інтеграція із застарілими бібліотеками (Legacy API):
Зовнішні бібліотеки чи клієнтські фреймворки, що очікують стандартні інтерфейси
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непридатний через $O(N)$ копіювання), виконання пакетних атомарних операцій над усією колекцією під єдиним замком та сумісність із legacy-кодом».
3 каверзних запитання з інтерв’ю та відповіді
1. Чому виклик for (String s : Collections.synchronizedList(list)) викидає ConcurrentModificationException, якщо список синхронізований?
Відповідь: Цикл for-each транслюється компілятором у послідовні виклики iterator(), hasNext() та next(). Метод syncList.iterator() потокобезпечно повертає ітератор, але внутрішній монітор списку звільняється одразу після завершення виклику методу iterator(). Якщо між кроками ітерації інший потік змінить список, лічильник модифікацій modCount зміниться, і наступний виклик next() викине ConcurrentModificationException. Обхід синхронізованої колекції обов’язково має загортатися в зовнішній блок synchronized (syncList).
2. У чому різниця між Vector та Collections.synchronizedList(new ArrayList<>())?
Відповідь:
Vectorсинхронізує методи на власному екземплярі (this), тоді якsynchronizedListсинхронізується на виділеному об’єктіmutex(що дозволяє передавати власний об’єкт блокування в перевантаженому конструкторі).Vectorза замовчуванням подвоює розмір масиву при переповненні ($\times 2$), тоді якArrayListвсередині обгортки збільшується в 1.5 раза.synchronizedListє патерном Декоратор і може огорнути будь-яку реалізаціюList(наприклад,LinkedList), тоді якVectorє монолітною структурою даних на базі масиву.
3. Чи є атомарною операція інкременту лічильника в Collections.synchronizedMap: map.put(key, map.get(key) + 1)?
Відповідь: Ні, категорично не є. Це три окремі дії: читання (get), додавання в регістрі процесора та запис (put). Монітор мапи захоплюється та звільняється двічі. Між викликом 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. - ❌ Витік посилання на базову огорнуту колекцію: Будь-яка зміна через початкове посилання ламає потокобезпеку обгортки.