В чем разница между ConcurrentHashMap и synchronizedMap в Java?
Что проверяют
Знаете ли вы, как устроена потокобезопасность в обоих вариантах, а не только лозунг «ConcurrentHashMap быстрее». Следом почти всегда спрашивают, как атомарно увеличить счётчик по ключу и почему в ConcurrentHashMap нельзя хранить null.
Как отвечать
Collections.synchronizedMap: обёртка над любойMap, каждый метод захватывает один общий монитор. С картой одновременно работает ровно один поток, чтение тоже ждёт;ConcurrentHashMap: с Java 8 сегментов нет. Вставка в пустую корзину идёт через CAS, при коллизии блокируется только первый узел корзины, чтение обходится без блокировок. Длинная цепочка превращается в дерево;- итерация: итератор обёртки берётся у исходной карты, у
HashMapон fail-fast, поэтому обходить её нужно внутриsynchronized (map) { ... }. ИтераторConcurrentHashMapслабо согласованный: исключение не бросает, но и снимком не является, изменения во время обхода может увидеть, а может и нет; - составные операции:
compute,merge,computeIfAbsentатомарны для ключа, счётчик пишется какmap.merge(key, 1, Integer::sum). Связкаgetплюсputатомарной не станет ни в одной из карт; null:ConcurrentHashMapне принимаетnullни в ключах, ни в значениях, иначеgetне отличил бы отсутствие ключа от пустого значения. Обёртка надHashMapnullпропускает;size(): под нагрузкой это оценка, для больших карт естьmappingCount().
Итог: обёртка годится, когда конкуренции почти нет или нужна блокировка всей карты сразу. Для кэшей и счётчиков под нагрузкой берут ConcurrentHashMap.
Где спотыкаются
Рассказывают про 16 сегментов и concurrencyLevel как про текущее устройство. Это Java 7, сейчас параметр влияет лишь на начальный размер таблицы.
Считают, что потокобезопасная карта делает безопасным и код вокруг неё. Проверка containsKey и следом put остаётся гонкой, нужен putIfAbsent или computeIfAbsent.
Внутри функции для computeIfAbsent меняют ту же карту. Контракт это запрещает, и можно получить IllegalStateException с текстом «Recursive update».