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

Как создать синхронизированную коллекцию?

1.0 Junior🔥 171 комментариев
#Коллекции#Многопоточность#Основы Java

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

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

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

Как создать синхронизированную коллекцию

Синхронизированная коллекция — это коллекция, которая потокобезопасна при одновременном доступе из нескольких потоков. В Java есть несколько подходов.

Способ 1: Collections.synchronized*()

import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;

// Синхронизированный List
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.add("item1");
syncList.add("item2");

// Синхронизированный Set
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
syncSet.add("item1");

// Синхронизированный Map
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
syncMap.put("key", 1);

Как это работает:

// Внутри Collections.synchronizedList() создаётся wrapper
public static <T> List<T> synchronizedList(List<T> list) {
    return new SynchronizedList<>(list);
}

// SynchronizedList оборачивает каждый метод
private static class SynchronizedList<E> extends SynchronizedCollection<E>
    implements List<E> {
    
    private final List<E> list;  // Оригинальный список
    
    @Override
    public void add(int index, E element) {
        synchronized(mutex) {  // Синхронизация по мьютексу
            list.add(index, element);
        }
    }
    
    @Override
    public E get(int index) {
        synchronized(mutex) {
            return list.get(index);
        }
    }
}

Проблема: итерация НЕ потокобезопасна

// ❌ ОПАСНО: ConcurrentModificationException
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
for (String item : syncList) {  // Начало итерации
    System.out.println(item);
    // Из другого потока: syncList.add("new");  ConcurrentModificationException!
}

// ✅ ПРАВИЛЬНО: явная синхронизация при итерации
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
synchronized(syncList) {  // Заморозили список
    for (String item : syncList) {
        System.out.println(item);
    }
}  // Разморозили

Способ 2: Explicit Locking (Lock)

import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.ArrayList;
import java.util.List;

public class SynchronizedListWithLock<T> {
    private final List<T> list = new ArrayList<>();
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    // Несколько читателей одновременно
    public T get(int index) {
        lock.readLock().lock();
        try {
            return list.get(index);
        } finally {
            lock.readLock().unlock();
        }
    }
    
    // Только один писатель
    public void add(T element) {
        lock.writeLock().lock();
        try {
            list.add(element);
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public int size() {
        lock.readLock().lock();
        try {
            return list.size();
        } finally {
            lock.readLock().unlock();
        }
    }
}

// Использование
SynchronizedListWithLock<String> list = new SynchronizedListWithLock<>();
list.add("item1");
String item = list.get(0);
int size = list.size();

Преимущества ReadWriteLock:

  • Несколько читателей могут читать одновременно
  • Писатель исключает всех (читателей и других писателей)
  • Лучше производительность для read-heavy операций

Способ 3: ConcurrentHashMap (для Map)

import java.util.concurrent.ConcurrentHashMap;

// ConcurrentHashMap — лучше, чем synchronized Map
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);

// Итерация потокобезопасна (снимок состояния)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

// Атомарные операции
map.putIfAbsent("key1", 10);  // Не перезапишет, если уже есть
map.replace("key1", 1, 100);   // Заменит только если значение == 1

Как работает:

// ConcurrentHashMap использует bucket-level locking
// Вместо одного мьютекса для всей карты,
// есть несколько мьютексов для разных «сегментов»

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// По умолчанию 16 сегментов
// Каждый сегмент защищён отдельным Lock'ом
// Потоки могут писать в разные сегменты параллельно

// Поток 1: map.put("a", 1)  → Lock сегмента 0
// Поток 2: map.put("b", 2)  → Lock сегмента 1
// Могут выполняться параллельно!

Способ 4: CopyOnWriteArrayList (для List)

import java.util.concurrent.CopyOnWriteArrayList;

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

// Итерация очень быстра
for (String item : list) {
    System.out.println(item);
}

// Модификация медленнее (копирует весь массив)
list.add("item3");  // Копирует старый + новый элемент
list.remove(0);     // Копирует без элемента 0

Как работает:

// При каждой модификации копируется весь массив
public class CopyOnWriteArrayList<E> {
    private volatile Object[] array;
    private final ReentrantLock lock = new ReentrantLock();
    
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            // Копируем старый массив + новый элемент
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    
    public E get(int index) {
        // НЕ нужна синхронизация при чтении
        return (E) getArray()[index];
    }
}

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

  • Много читаний, мало модификаций (write-rarely)
  • Малое количество элементов
  • Итераторы не должны видеть новые элементы

Способ 5: ConcurrentLinkedQueue (для Queue)

import java.util.concurrent.ConcurrentLinkedQueue;

// Потокобезопасная очередь без блокировок (lock-free)
Queue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("item1");
queue.offer("item2");

String item = queue.poll();  // Получить и удалить
String peek = queue.peek();  // Получить без удаления

// Итерация потокобезопасна
for (String item : queue) {
    System.out.println(item);
}

Сравнительная таблица

КоллекцияМетод синхронизацииИтерацияПроизводительность
synchronizedList()One lock❌ ОпаснаНизкая
CopyOnWriteArrayListCopy-on-write✅ БезопаснаНизкая на write
ConcurrentHashMapSegment locks✅ БезопаснаВысокая
ConcurrentLinkedQueueLock-free✅ БезопаснаОчень высокая
Custom with ReadWriteLockReadWriteLock✅ БезопаснаЗависит от коэфф.

Рекомендации

// ✅ Для Map — используй ConcurrentHashMap
Map<String, Integer> map = new ConcurrentHashMap<>();

// ✅ Для List с много модификаций — synchronized
List<String> list = Collections.synchronizedList(new ArrayList<>());

// ✅ Для List с мало модификаций — CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();

// ✅ Для Queue — ConcurrentLinkedQueue
Queue<String> queue = new ConcurrentLinkedQueue<>();

// ✅ Для Custom logic — ReentrantReadWriteLock
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();   // Читают много
lock.writeLock().lock();  // Пишет редко

Итоги

  1. Collections.synchronized()* — простой способ, но медленный
  2. ConcurrentHashMap — лучше для Map, lock-free
  3. CopyOnWriteArrayList — для read-heavy List
  4. ConcurrentLinkedQueue — для очереди, очень быстро
  5. ReadWriteLock — для кастомной логики
  6. Всегда тестируй под нагрузкой в многопоточной среде
  7. Избегай synchronized(collection) при итерации — замораживает весь список