← Назад к вопросам

Какие знаешь реализации листов, которые поддерживают внутреннее копирование во время итерирования?

3.0 Senior🔥 81 комментариев
#Коллекции

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Реализации List с внутренним копированием во время итерирования

Copy-On-Write коллекции решают проблему потокобезопасности при одновременном чтении и записи. Ключевая идея: вместо блокировки при каждой записи, создаётся копия массива при каждой модификации. Это даёт эффективное чтение за счёт более дорогой записи.

1. CopyOnWriteArrayList

Это основная реализация Copy-On-Write паттерна в Java Collections Framework:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, Serializable

Как работает

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

// ИТЕРИРОВАНИЕ
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
    
    // Внутренне: iterator хранит снимок массива из момента создания
    // Поэтому даже если мы модифицируем list, iterator не видит изменений
}

// МОДИФИКАЦИЯ во время итерирования — БЕЗОПАСНО!
list.add("D");  // Создаёт новую копию внутреннего массива
list.remove(0); // Ещё одна копия
list.set(1, "B-modified"); // И ещё одна копия

// Iterator по-прежнему итерирует старую копию
while (iterator.hasNext()) {
    System.out.println(iterator.next());  // A, B, C (не видит D)
}

Реализация модификации

public class CopyOnWriteArrayList<E> {
    private transient volatile Object[] array;  // Volatile!
    
    public boolean add(E e) {
        synchronized(this) {  // Только одна запись одновременно
            Object[] elements = array;
            int len = elements.length;
            
            // КОПИРОВАНИЕ: создаём новый массив
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            
            // Атомично заменяем reference
            array = newElements;
            return true;
        }
    }
    
    // ЧТЕНИЕ — безопасно без синхронизации
    public E get(int index) {
        return (E) array[index];  // Просто читаем, никаких блокировок
    }
}

Создание Iterator

public class CopyOnWriteArrayList<E> {
    public Iterator<E> iterator() {
        // Снимок массива в момент создания iterator
        return new COWIterator<E>(array, 0);
    }
}

private static class COWIterator<E> implements Iterator<E> {
    private final Object[] snapshot;  // Неизменяемая копия
    private int cursor;
    
    public COWIterator(Object[] elements, int initialCursor) {
        this.snapshot = elements;  // Снимок в момент создания
        this.cursor = initialCursor;
    }
    
    public boolean hasNext() {
        return cursor < snapshot.length;
    }
    
    public E next() {
        if (cursor >= snapshot.length)
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
    
    // remove() не поддерживается!
    public void remove() {
        throw new UnsupportedOperationException();
    }
}

Пример безопасной итерации

CopyOnWriteArrayList<String> cities = new CopyOnWriteArrayList<>();
cities.add("Moscow");
cities.add("SPB");
cities.add("Kazan");

// Поток 1: итерирует
Thread t1 = new Thread(() -> {
    for (String city : cities) {
        System.out.println("T1: " + city);
        try { Thread.sleep(100); } catch (InterruptedException e) {}
    }
});

// Поток 2: модифицирует список
Thread t2 = new Thread(() -> {
    cities.add("Yekaterinburg");
    cities.add("Novosibirsk");
    cities.remove("SPB");
});

t1.start();
Thread.sleep(50);
t2.start();
t1.join();
t2.join();

// T1 итерировал старый снимок: Moscow, SPB, Kazan
// T2 модифицировал список без блокирования T1

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

Хорошо для:

  • Много читателей, мало писателей
  • Итерирование в multi-threaded окружении
  • Слушатели событий (Event listeners)
  • Кэши, которые редко обновляются

Плохо для:

  • Частые модификации (O(n) за каждую операцию)
  • Большие списки (копирование всего массива дорого)

2. CopyOnWriteArraySet

Аналог CopyOnWriteArrayList, но для Set:

public class CopyOnWriteArraySet<E> extends AbstractSet<E> {
    private final CopyOnWriteArrayList<E> al;  // Внутренне использует List
    
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<>();
    }
    
    public boolean add(E e) {
        return al.addIfAbsent(e);  // Добавляет только если отсутствует
    }
}

// Пример
CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
set.add(1);
set.add(2);
set.add(3);

for (Integer val : set) {
    System.out.println(val);
    set.add(4);  // Безопасно!
}

3. Collections.synchronizedList vs CopyOnWriteArrayList

Разница в подходах:

// synchronizedList — блокирует каждую операцию
List<String> syncList = Collections.synchronizedList(new ArrayList<>());

// Если итерируем, нужна явная синхронизация:
List<String> list = Collections.synchronizedList(new ArrayList<>());
synchronized(list) {  //必須!
    for (String item : list) {
        // ConcurrentModificationException без этого!
        System.out.println(item);
    }
}

// CopyOnWriteArrayList — не требует явной синхронизации
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();nfor (String item : cowList) {  // Безопасно БЕЗ синхронизации
    System.out.println(item);
    cowList.add("new");  // Тоже безопасно!
}

4. Снимки и Snapshot Iterator Pattern

Это общий паттерн для безопасного итерирования:

public class SnapshotList<E> {
    private List<E> data = new ArrayList<>();
    private final Object lock = new Object();
    
    public void add(E element) {
        synchronized(lock) {
            data.add(element);
        }
    }
    
    // Iterator получает снимок в момент создания
    public Iterator<E> iterator() {
        synchronized(lock) {
            // Копируем данные
            return new ArrayList<>(data).iterator();
        }
    }
    
    // Пример использования
    public static void main(String[] args) throws InterruptedException {
        SnapshotList<Integer> list = new SnapshotList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        
        Iterator<Integer> it = list.iterator();
        list.add(4);  // Список изменился
        
        while (it.hasNext()) {
            System.out.println(it.next());  // 1, 2, 3 (без 4)
        }
    }
}

5. ConcurrentHashMap.KeySetView (альтернатива)

Для других типов данных (key-value) есть ConcurrentHashMap:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
map.put("B", 2);

// Iterator не выбросит ConcurrentModificationException
for (String key : map.keySet()) {
    System.out.println(key);
    map.put("C", 3);  // Изменяем во время итерирования
}

// Map.values() тоже безопасен
for (Integer val : map.values()) {
    System.out.println(val);
}

6. Производительность: Сравнение

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
public class ListBenchmark {
    
    @Benchmark
    public void cowList_Read(Blackhole bh) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 1000; i++) {
            list.add(i);
        }
        for (Integer val : list) {
            bh.consume(val);  // Быстро для чтения
        }
    }
    
    @Benchmark
    public void cowList_Write(Blackhole bh) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 1000; i++) {
            list.add(i);  // МЕДЛЕННО! O(n) за операцию
        }
        bh.consume(list.size());
    }
    
    @Benchmark
    public void syncList_Write() {
        List<Integer> list = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 1000; i++) {
            list.add(i);  // Быстрее: O(1) за операцию
        }
    }
}

// Результаты примерно:
// cowList_Read:   ~0.5 ms    ✓ Отлично
// cowList_Write:  ~3.5 ms    ✗ Плохо
// syncList_Write: ~1.0 ms    ✓ Хорошо

7. Реальные примеры использования

Event Listeners (частый case)

public class EventBus {
    private final CopyOnWriteArrayList<EventListener> listeners 
        = new CopyOnWriteArrayList<>();
    
    public void subscribe(EventListener listener) {
        listeners.add(listener);  // Редко
    }
    
    public void unsubscribe(EventListener listener) {
        listeners.remove(listener);  // Редко
    }
    
    public void publish(Event event) {
        // Много читателей, может быть добавлен слушатель
        // во время публикации
        for (EventListener listener : listeners) {  // Часто
            listener.onEvent(event);
        }
    }
}

Кэш с редкими обновлениями

public class CachedDataList {
    private CopyOnWriteArrayList<CachedEntry> entries 
        = new CopyOnWriteArrayList<>();
    
    public List<CachedEntry> getAll() {
        return new ArrayList<>(entries);  // или просто итерируем
    }
    
    public void refresh() {
        // Переупаковываем весь список (редко)
        CopyOnWriteArrayList<CachedEntry> newEntries 
            = new CopyOnWriteArrayList<>();
        // ... загружаем данные ...
        entries = newEntries;
    }
}

8. Ограничения и недостатки

// 1. Iterator.remove() не поддерживается
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
Iterator<String> it = list.iterator();
it.next();
it.remove();  // UnsupportedOperationException!

// 2. Высокое потребление памяти (копии при каждой записи)
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100000; i++) {
    list.add(i);  // Каждый add = копия всего массива! Плохо!
}

// 3. Снимок может быть старым
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
Iterator<String> it = list.iterator();

list.add("B");  // Добавили
list.add("C");

// Iterator всё ещё видит только [A]
while (it.hasNext()) {
    System.out.println(it.next());  // Только A
}

Выводы

CopyOnWriteArrayList — отличный выбор когда:

  • ✓ Много читателей, мало писателей
  • ✓ Размер списка небольшой
  • ✓ Нужна безопасная итерация без явной синхронизации
  • ✓ Слушатели событий

Используй альтернативы когда:

  • Collections.synchronizedList() — нужен frequent write + rare iteration
  • ConcurrentHashMap — для key-value с частыми обновлениями
  • Synchronized блоки — полный контроль над синхронизацией
  • Stream API с filter/map — обработка данных

Это мощный паттерн, широко используемый в production коде, особенно в реактивных и event-driven системах.