Collections.synchronizedMap: примеры (JAVA)

Синхронизация Map в Java через Collections.synchronizedMap
Раздел: Коллекции (Collection Framework) - Map
Collections.synchronizedMap(Map m): Map

Описание и поведение Collections.synchronizedMap

Метод Collections.synchronizedMap предоставляет потокобезопасную оболочку вокруг существующей реализации интерфейса Map. Подпись метода: public static Map synchronizedMap(Map m). Входной параметр m - ссылка на исходную карту, которая будет использоваться как хранилище данных. При передаче null будет выброшено java.lang.NullPointerException.

Возвращаемое значение - объект Map, реализующий ту же «логику хранения», что и исходная карта, но все публичные методы которого синхронизированы на общем мьютексе. Это означает, что при вызове методов, таких как get, put, remove, containsKey и др., используется монитор (lock) для последовательного доступа потоков. Возвращаемая карта является представлением (view) исходной карты: изменения в исходной коллекции отражаются в возвращаемой и наоборот.

Особенности поведения и ограничения:

  • Мьютекс: у возвращаемой обёртки используется внутренний объект-мьютекс. В стандартной реализации Collections.synchronizedMap мьютекс - сам объект оболочки, поэтому при внешней синхронизации требуется блокировать именно обёртку: synchronized(synchronizedMap) { ... }.
  • Итераторы: итераторы, полученные от возвращаемой карты (entrySet().iterator(), keySet().iterator(), values().iterator()), не синхронизированы автоматически. Для безопасной итерации нужно явно синхронизировать блок на обёртке: synchronized(map) { for (Iterator i = map.entrySet().iterator(); i.hasNext();) ... }.
  • Сериализация: возвращаемая карта сериализуема, если сериализуема исходная карта.
  • Производительность: синхронизированная обёртка использует один глобальный мьютекс для всех операций, что может стать узким местом при высокой многопоточности. Для высоконагруженных сценариев обычно предпочтителен ConcurrentHashMap.
  • Поддержка null: способность хранить null-ключи и значения определяется исходной картой. Например, HashMap допускает null, ConcurrentHashMap - не допускает.

Типичные сигнатуры и эффекты:

  • Collections.synchronizedMap(Map<K,V> m) - возвращает потокобезопасную обёртку; выбрасывает NullPointerException при m == null.

Примеры базового применения synchronizedMap

Создание обёртки и базовые операции.

Map<String,Integer> raw = new HashMap<>();
Map<String,Integer> sync = Collections.synchronizedMap(raw);
sync.put("a", 1);
sync.put("b", 2);
System.out.println(sync.get("a"));
1

Итерация с внешней синхронизацией - обязательна для корректности.

Map<String,Integer> m = Collections.synchronizedMap(new HashMap<>());
m.put("x", 10); m.put("y", 20);
// Неправильно: возможны проблемы при одновременном доступе
for (Map.Entry<String,Integer> e : m.entrySet()) {
    System.out.println(e.getKey()+":"+e.getValue());
}

// Правильно
synchronized(m) {
    for (Map.Entry<String,Integer> e : m.entrySet()) {
        System.out.println(e.getKey()+":"+e.getValue());
    }
}
x:10
y:20

Сравнение с ConcurrentHashMap: синхронизированная обёртка блокирует все операции, ConcurrentHashMap обеспечивает более высокую пропускную способность для чтений и частичных записей.

Аналоги в Java и их отличия

  • ConcurrentHashMap - конкурентная реализация Map, не использующая один глобальный мьютекс; предпочтительна при высокой конкуренции потоков и частых операциях чтения.
  • Collections.synchronizedSortedMap и Collections.synchronizedNavigableMap - синхронизированные обёртки для SortedMap и NavigableMap соответственно; применяются, если нужен упорядоченный функционал с синхронизацией.
  • Hashtable - устаревшая коллекция, все публичные методы синхронизированы; содержит наследие API и обычно заменяется на ConcurrentHashMap или synchronizedMap при совместимости.

Аналоги в других языках и отличия

Краткий обзор альтернатив на популярных языках с примерами.

  • Python: стандартный dict не синхронизирован на уровне множественных потоков; для потокобезопасности используют threading.Lock или collections.OrderedDict вместе с блокировкой. Пример:
from threading import Lock
m = {}
lock = Lock()
with lock:
    m['a'] = 1
print(m['a'])
1
  • JavaScript (Node.js): в основном однопоточная модель, Map не синхронизируется; для Worker Threads и общего состояния применяют внешние механизмы синхронизации или обмен сообщениями.
  • PHP: нет встроенных потокобезопасных карт в стандартном режиме; в многопоточных расширениях используются инструменты вроде pthreads или системное кеширование (Redis, Memcached).
  • C#: ConcurrentDictionary как прямой аналог ConcurrentHashMap; есть Hashtable.Synchronized возвращающий обёртку, похожую на Collections.synchronizedMap.
  • Go: встроенная map не безопасна для конкурентного доступа; стандартная альтернатива - sync.Map для специфичных задач или map + sync.RWMutex для общей гибкости.
  • Kotlin: использует Java-коллекции; применимы те же варианты: Collections.synchronizedMap и ConcurrentHashMap.
  • Lua: стандартные таблицы не синхронизированы; в многопоточных средах применяются внешние библиотеки или очереди сообщений между состояниями.
  • SQL: концептуально похожие потребности решаются транзакциями и блокировкой на уровне базы данных, что отличается от объектной синхронизации в памяти.
  • Кодовые примеры для Go и C#:

    // Go: использование sync.Map
    var m sync.Map
    m.Store("k", 42)
    v, _ := m.Load("k")
    fmt.Println(v)
    
    42
    
    // C#: ConcurrentDictionary
    var d = new System.Collections.Concurrent.ConcurrentDictionary();
    d["k"] = 5;
    Console.WriteLine(d["k"]);
    
    5
    

Типичные ошибки и неочевидные проблемы

  • Отсутствие синхронизации при итерации: получение итератора и обход без synchronized(wrapper) может привести к ConcurrentModificationException или некорректному результату в многопоточной среде. Пример неправильного кода:
Map<Integer,String> m = Collections.synchronizedMap(new HashMap<>());
m.put(1, "a");
for (Map.Entry<Integer,String> e : m.entrySet()) {
    m.put(2, "b"); // модификация во время итерации
}
Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1598)
    ...
  • Синхронизация не на той инстанции: некоторые ожидают, что нужно синхронизировать на исходном raw-объекте; правильное - синхронизировать на возвращаемой обёртке. Иначе гарантии не выполняются.
  • Ожидание производительности: использование synchronizedMap в сценариях с большим количеством потоков и операциями чтения/записи может привести к узкому месту. В таких случаях ConcurrentHashMap или специализированные структуры предпочтительнее.
  • Null-ключи и значения: поведение зависит от базовой реализации; при замене на ConcurrentHashMap код может неожиданно начать бросать NullPointerException.

Изменения и история метода

Метод Collections.synchronizedMap присутствует в Java с ранних версий (коллекции были введены в JDK 1.2). В базовом поведении изменений немного: метод по-прежнему возвращает синхронизированную обёртку вокруг переданной карты. В более новых релизах Java появились дополнительные синхронизированные обёртки для SortedMap и NavigableMap, а также усовершенствования в других коллекциях. Для конкурентных сценариев рекомендуются структуры из java.util.concurrent (ConcurrentHashMap и др.), которые развивались и оптимизировались в более поздних версиях JVM.

Расширенные и необычные примеры использования

1) Обёртка для LRU-кэша на базе LinkedHashMap. При использовании в многопоточной среде обёртка обеспечивает синхронизацию, однако важно помнить о synchronize при итерации.

Пример java
Map<String,Integer> lru = Collections.synchronizedMap(
    new LinkedHashMap<String,Integer>(16, 0.75f, true) {
        protected boolean removeEldestEntry(Map.Entry<String,Integer> eldest) {
            return size() > 3;
        }
    }
);
// Заполнение
lru.put("a",1); lru.put("b",2); lru.put("c",3); lru.put("d",4);
// Содержимое
synchronized(lru) {
    for (Map.Entry<String,Integer> e : lru.entrySet()) {
        System.out.println(e.getKey());
    }
}
b
c
d

Пояснение: при ограничении размера 3 самый старый элемент удалён. Доступ к map обёрнут синхронизацией перед итерацией.

2) Совместное использование с внешним мьютексом. В стандартной коллекции мьютекс - сама обёртка. Дальше пример, как создать обёртку с общим мьютексом вручную (внутри класса):

Пример java
Map<String,Integer> base = new HashMap<>();
Object mutex = new Object();
Map<String,Integer> custom = new AbstractMap<>() {
    public Set<Entry<String,Integer>> entrySet() { return base.entrySet(); }
    public Integer get(Object k) { synchronized(mutex) { return base.get(k); } }
    public Integer put(String k, Integer v) { synchronized(mutex) { return base.put(k,v); } }
};
// Теперь можно синхронизировать на mutex во всех смежных структурах
(нет вывода, демонстрация идеи)

Пояснение: при наличии нескольких связанных структур данных можно синхронизировать их на одном объекте для атомарных комплексных операций.

3) Обёртка вокруг TreeMap с навигацией (synchronizedSortedMap):

Пример java
SortedMap<Integer,String> sm = Collections.synchronizedSortedMap(new TreeMap<>());
sm.put(1,"one"); sm.put(2,"two");
synchronized(sm) {
    System.out.println(sm.firstKey());
}
1

4) Нежелательная практика: ожидание освобождения внешнего lock при блокировке внутри synchronizedMap. При вызове пользовательского кода изнутри синхронизированного метода есть риск дедлока, если этот код попытается захватить другие внешние ресурсы.

5) Пример взаимодействия с ConcurrentHashMap: переключение с synchronizedMap на ConcurrentHashMap может потребовать изменений в коде, если раньше использовалась внешняя синхронизация для итерации, так как ConcurrentHashMap возвращает слабоконсистентные итераторы, не выбрасывающие ConcurrentModificationException.

Пример java
Map<String,Integer> sync = Collections.synchronizedMap(new HashMap<>());
Map<String,Integer> conc = new ConcurrentHashMap<>();
// При использовании conc итерация не требует внешней блокировки
for (Map.Entry<String,Integer> e : conc.entrySet()) {
    // возможны несогласованные, но безопасные снимки данных
}
(нет определённого вывода, зависит от состояния conc)

джава Collections.synchronizedMap function comments

En
Collections.synchronizedMap Returns a synchronized (thread-safe) map backed by the specified map