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

Что такое CopyOnWriteArrayList?

4. Iterator = snapshot, не live view 5. Memory spike при записи → OOM риск 6. Stale data — допустимо для вашего сценария? 7. Альтернативы: ConcurrentLinkedQueue, synchronizedList

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

🟢 Junior Level

CopyOnWriteArrayList — потокобезопасный список, который копирует массив при каждой записи.

Простая аналогия: Фотография. Читатели смотрят на фото (старую версию), а автор делает новую фотографию (копию с изменениями).

List<String> list = new CopyOnWriteArrayList<>();

// Чтение: быстро, без блокировок.
// volatile Object[] array → get() = простое чтение из массива, без атомарных операций и synchronized.
String s = list.get(0);

// Запись: создаёт копию массива
list.add("A");  // Копия → добавление → замена

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

  • Много чтений, мало записей
  • Списки слушателей (listeners)

🟡 Middle Level

Как работает

// Внутри: volatile Object[] array

// Чтение (get):
return array[index];  // Без блокировок!

// Запись (add):
1. Lock
2. newArray = Arrays.copyOf(array, size + 1)
3. newArray[size] = element
4. array = newArray  // volatile запись
5. Unlock

Iterator = Snapshot

List<String> list = new CopyOnWriteArrayList<>();
list.add("A");

Iterator<String> it = list.iterator();  // Snapshot!
list.add("B");  // Изменили список

it.next();  // → "A" (итератор НЕ видит "B"!)
// → Fail-Safe, никогда не бросает Exception

// it.remove() → UnsupportedOperationException!

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

// ✅ Списки слушателей
List<Listener> listeners = new CopyOnWriteArrayList<>();

// Чтения (уведомления) >>> записей (подписка)
for (Listener l : listeners) {  // Быстро!
    l.onEvent();
}

// ✅ Редко меняющиеся кэши

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

// ❌ Частые записи
list.add(e);  // Копия массива каждый раз!
// → O(n) на запись
// → Удвоение памяти временно

// ✅ Альтернативы:
ConcurrentLinkedQueue<String> queue;
Collections.synchronizedList(list);

**Ключевое отличие:** synchronizedList блокирует каждое чтение, COWAL не блокирует. synchronizedList итератор бросает CME при модификации, COWAL  нет (snapshot).

🔴 Senior Level

Memory Footprint

// При записи:
oldArray: 1 млн элементов  4 МБ
newArray: 1 млн + 1  4 МБ
// → 8 МБ временно!

// Для больших списков → OOM риск
// → GC давление при частых записях

Happens-Before гарантия

// volatile array = Happens-Before
// Запись в одном потоке → видна всем в других

// Но: окно между копированием и записью
// → Читатели могут видеть "старые" данные

Stale Data проблема

  • Fail-Safe = итератор никогда не бросает ConcurrentModificationException (работает с копией)
  • Stale Data = «устаревшие данные» — читатель видит версию массива на момент создания итератора
  • Eventual Consistency = данные станут актуальными не мгновенно, а после следующей записи
// Поток 1: читает array (старая версия)
// Поток 2: копирует, модифицирует, записывает новый array
// Поток 1: всё ещё читает старый array

 Консистентность eventual, не strong!

Iterator ограничения

// Fail-Safe:
// → Никогда ConcurrentModificationException
// → Но: не видит изменения после создания

// Методы итератора:
it.remove()  // → UnsupportedOperationException!
it.add()     // → UnsupportedOperationException!
it.set()     // → UnsupportedOperationException!

Production Experience

Реальный сценарий: Listener list

// Event Bus: 1000 подписчиков
// Уведомления: 100,000/сек
// Подписка/отписка: 1/сек

// Соотношение 100,000 чтений к 1 записи → копирование массива (O(n)) происходит 1 раз на 100,000 операций → оверхед ничтожен.

CopyOnWriteArrayList: 
   Чтение: O(1), lock-free
   Запись: O(n), но редко!
   Идеально подходит!

Best Practices

  1. Преимущественно для read-heavy сценариев. Допустимо для маленьких коллекций (< 100) даже при умеренных записях.
  2. Списки слушателей — идеальное применение
  3. Избегайте при частых записях
  4. Iterator = snapshot, не live view
  5. Memory spike при записи → OOM риск
  6. Stale data — допустимо для вашего сценария?
  7. Альтернативы: ConcurrentLinkedQueue, synchronizedList

Резюме для Senior

  • Copy-On-Write = копия массива при записи
  • Чтение = O(1), lock-free, volatile array
  • Запись = O(n), копирование + замена
  • Iterator = snapshot, fail-safe
  • Память = удвоение временно при записи
  • Listeners = идеальное применение
  • Stale data = eventual consistency
  • Избегайте для write-heavy сценариев

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

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

  • CopyOnWriteArrayList — при каждой записи (add/set/remove) создаёт копию внутреннего массива, чтение lock-free через volatile
  • Чтение = O(1), без блокировок. Запись = O(n), копирование + замена массива + lock
  • Iterator = snapshot на момент создания, fail-safe (не бросает CME), не видит изменения после создания
  • Iterator НЕ поддерживает remove/add/set — UnsupportedOperationException
  • Идеальное применение: списки слушателей (listeners) — ratio чтений к записям 100,000:1
  • Memory: при записи временно удваивается память (oldArray + newArray) — OOM риск для больших списков
  • Happens-Before гарантия через volatile array, но eventual consistency — читатели могут видеть старые данные
  • Альтернативы для write-heavy: ConcurrentLinkedQueue, Collections.synchronizedList

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

  • Почему iterator не видит добавленные элементы? — Iterator работает со snapshot (копией массива на момент создания). Новые элементы добавляются в новый массив — snapshot их не видит.
  • Чем COWAL отличается от synchronizedList? — synchronizedList блокирует КАЖДОЕ чтение. COWAL не блокирует чтения. synchronizedList iterator бросает CME при модификации, COWAL — нет (snapshot).
  • Когда COWAL — плохой выбор? — При частых записях: каждая запись = копирование всего массива. Для 1 млн элементов = 4 МБ копирования + временное удвоение памяти.
  • Что такое eventual consistency в COWAL? — Поток читает старый массив, другой поток записывает новый. Читатель увидит новые данные не мгновенно, а только после следующей ссылки на array.

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

  • ❌ «COWAL блокирует чтение» — нет, чтение полностью lock-free через volatile
  • ❌ «Iterator COWAL показывает live данные» — это snapshot, не видит изменения после создания
  • ❌ «COWAL подходит для частых записей» — O(n) на запись + удвоение памяти, используйте ConcurrentLinkedQueue
  • ❌ «COWAL iterator поддерживает remove()» — нет, UnsupportedOperationException для всех модификаций

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

  • [[18. Что такое ConcurrentHashMap]]
  • [[19. Как ConcurrentHashMap обеспечивает thread-safety]]
  • [[14. Что такое Map и какие реализации существуют]]