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

Какие знаешь варианты синхронизации коллекций?

2.0 Middle🔥 191 комментариев
#Коллекции#Многопоточность

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

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

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

Варианты синхронизации коллекций в Java

Коллекции Java по умолчанию не потокобезопасны. Если несколько потоков обращаются к одной коллекции одновременно и хотя бы один её модифицирует, может произойти ConcurrentModificationException или повреждение данных. Существует несколько способов обеспечить потокобезопасность.

1. Collections.synchronizedXXX (Wrapper синхронизация)

Это обёртки вокруг обычных коллекций, которые добавляют locks ко всем методам.

Collections.synchronizedList

import java.util.*;

List<String> unsafeList = new ArrayList<>();
List<String> safeList = Collections.synchronizedList(unsafeList);

safeList.add("item1");
safeList.get(0);

Плюсы:

  • Простое использование
  • Работает со всеми методами
  • Минимум изменений в коде

Минусы:

  • Слабая производительность (глобальный lock)
  • Не защищает от race condition при составных операциях
  • ConcurrentModificationException при итерации

Проблема с составными операциями

List<String> list = Collections.synchronizedList(new ArrayList<>());

if (!list.contains("item")) {
    list.add("item");  // Race condition: между contains и add
}

// Правильно: синхронизируй итерацию
synchronized(list) {
    if (!list.contains("item")) {
        list.add("item");
    }
}

Все варианты

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
SortedSet<String> syncSortedSet = Collections.synchronizedSortedSet(new TreeSet<>());
SortedMap<String, Integer> syncSortedMap = Collections.synchronizedSortedMap(new TreeMap<>());

2. Concurrent* классы (рекомендуется)

В пакете java.util.concurrent есть оптимизированные потокобезопасные коллекции с лучшей производительностью.

CopyOnWriteArrayList

import java.util.concurrent.*;

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

list.add("item1");
list.add("item2");
list.remove(0);

// Итерация безопасна БЕЗ явной синхронизации
for (String item : list) {
    System.out.println(item);
}

Механизм: При каждом изменении создаётся новая копия массива. Чтения читают старую версию.

Плюсы:

  • Итерация БЕЗ ConcurrentModificationException
  • Отличная производительность для чтения
  • Нет явных locks

Минусы:

  • Дорогие операции записи (O(n) копирование)
  • Использует много памяти
  • Не подходит для частых изменений

Когда использовать: Много чтений, мало записей (слушатели событий)

ConcurrentHashMap

Map<String, Integer> map = new ConcurrentHashMap<>();

map.put("key1", 1);
map.put("key2", 2);
int value = map.get("key1");

for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

Механизм: Segmented locking — карта разделена на сегменты (по умолчанию 16), каждый имеет свой lock.

Плюсы:

  • Хорошая производительность (многопоточность)
  • Итерация безопасна
  • Масштабируется лучше, чем synchronized Map

Минусы:

  • Слабее при очень частой записи

ConcurrentLinkedQueue

Queue<Integer> queue = new ConcurrentLinkedQueue<>();

queue.offer(1);
queue.offer(2);
Integer head = queue.poll();

Механизм: Lock-free очередь (использует compare-and-swap операции CAS)

Плюсы:

  • Очень быстрая даже при большом конкурсе
  • Не использует явные locks

Минусы:

  • Только для FIFO сценариев
  • Нет блокирующих операций

BlockingQueue семейство

import java.util.concurrent.*;

BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
BlockingQueue<Integer> boundedQueue = new LinkedBlockingQueue<>(100);

// Поток-производитель
new Thread(() -> {
    try {
        for (int i = 0; i < 1000; i++) {
            queue.put(i);  // Блокируется если очередь полна
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

// Поток-потребитель
new Thread(() -> {
    try {
        while (true) {
            Integer item = queue.take();  // Блокируется если очередь пуста
            System.out.println("Got: " + item);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

Типы BlockingQueue:

  • LinkedBlockingQueue — неограниченная, на основе связных списков
  • ArrayBlockingQueue — ограниченная, на основе массива
  • PriorityBlockingQueue — с приоритетом
  • SynchronousQueue — нет буфера, put() блокирует до take()

Когда использовать: Producer-Consumer паттерн, thread pools

3. synchronized ключевое слово (не рекомендуется)

Старый способ, для новых проектов используй Concurrent классы.

public class SynchronizedList<T> {
    private List<T> items = new ArrayList<>();
    
    public synchronized void add(T item) {
        items.add(item);
    }
    
    public synchronized T get(int index) {
        return items.get(index);
    }
    
    // Более гибко: синхронизировать блок
    public List<T> getAll() {
        synchronized(items) {
            return new ArrayList<>(items);
        }
    }
}

Плюсы:

  • Полный контроль
  • Точная синхронизация

Минусы:

  • Низкая производительность (глобальный lock)
  • Легко допустить deadlock
  • Сложный для отладки код

4. ReentrantReadWriteLock (специалист для read-heavy)

import java.util.concurrent.locks.*;

public class ReadWriteList<T> {
    private List<T> items = new ArrayList<>();
    private ReadWriteLock lock = new ReentrantReadWriteLock();
    
    // Много потоков могут читать одновременно
    public T get(int index) {
        lock.readLock().lock();
        try {
            return items.get(index);
        } finally {
            lock.readLock().unlock();
        }
    }
    
    // Только один поток может писать
    public void add(T item) {
        lock.writeLock().lock();
        try {
            items.add(item);
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Плюсы:

  • Много читателей могут работать параллельно
  • Быстрые чтения

Минусы:

  • Дороже в управлении
  • Сложнее кода
  • Может быть медленнее на write-heavy сценариях

Когда использовать: Кэш (99% чтений, 1% записей)

5. Immutable коллекции (лучшее решение)

// JDK 9+
List<String> immutableList = List.of("a", "b", "c");
Set<String> immutableSet = Set.of("x", "y", "z");
Map<String, Integer> immutableMap = Map.of("key1", 1, "key2", 2);

// immutableList.add("d");  // UnsupportedOperationException

Плюсы:

  • Абсолютная потокобезопасность
  • Нет locks
  • Максимальная производительность
  • Просто рассуждать о коде

Минусы:

  • Нельзя модифицировать
  • Нужна новая коллекция для каждого изменения

Когда использовать: Везде, где данные не меняются

Таблица сравнения

ТипПотокобезопасностьПроизводительностьЧтениеЗаписьИтерация
ArrayListНетОтличнаяОтличнаяОтличнаяОтличная
synchronized ListДаСлабаяСлабаяСлабаяНет*
CopyOnWriteArrayListДаОтличнаяОтличнаяСлабаяДа
HashMapНетОтличнаяОтличнаяОтличнаяОтличная
ConcurrentHashMapДаОтличнаяОтличнаяХорошаяДа
LinkedBlockingQueueДаХорошаяХорошаяХорошаяДа
ConcurrentLinkedQueueДаОтличнаяОтличнаяОтличнаяДа
ImmutableДаОтличнаяОтличнаяN/AДа

*ConcurrentModificationException без явного лока

Как выбрать?

Нужна ли потокобезопасность?

НЕТ → ArrayList, HashMap

ДА
  Данные никогда не меняются?
  → List.of(), Set.of(), Map.of()
  
  Много чтений, редко пишем?
  → ConcurrentHashMap или CopyOnWriteArrayList
  
  Очередь (Producer-Consumer)?
  → BlockingQueue (LinkedBlockingQueue)
  
  Map с частыми операциями?
  → ConcurrentHashMap
  
  Read-Write сценарий?
  → ReentrantReadWriteLock
  
  Множество операций одновременно?
  → ConcurrentLinkedQueue (Lock-free)

Пример: Потокобезопасный кэш

import java.util.concurrent.ConcurrentHashMap;

public class ThreadSafeCache<K, V> {
    private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
    
    public V get(K key) {
        return cache.get(key);
    }
    
    public void put(K key, V value) {
        cache.put(key, value);
    }
    
    public V getOrCompute(K key, Function<K, V> loader) {
        return cache.computeIfAbsent(key, loader);
    }
}

ЗАБЫТЬ: Hashtable

// НИКОГДА не используй
Hashtable<String, Integer> oldHashtable = new Hashtable<>();

// Используй ConcurrentHashMap вместо
ConcurrentHashMap<String, Integer> modernMap = new ConcurrentHashMap<>();

Hashtable — древний класс (Java 1.0), всё синхронизировано глобальным локом. ConcurrentHashMap намного эффективнее.

Какие знаешь варианты синхронизации коллекций? | PrepBro