В чем отличие ConcurrentHashMap от HashMap и как реализована внутренняя блокировка
Что проверяют
Знаете ли вы, что ConcurrentHashMap перестал делиться на сегменты ещё в Java 8. Ответ про сегменты выдаёт знание по старым статьям.
Как отвечать
HashMap не потокобезопасен: одновременная запись из двух потоков ломает внутреннюю структуру, а в старых версиях могла привести к зацикливанию при расширении. ConcurrentHashMap рассчитан на параллельный доступ и делает это без глобальной блокировки.
Как устроена синхронизация в современных версиях:
- запись в пустую корзину идёт через атомарную операцию сравнения с обменом, без блокировок вообще;
- если корзина занята, блокируется только её первый узел через
synchronized, то есть параллельные записи в разные корзины друг друга не ждут; - чтение не блокируется никогда: узлы хранят значения в
volatile-полях; - расширение таблицы происходит совместно: потоки, наткнувшиеся на переносимую корзину, помогают переносу вместо ожидания;
- размер считается не одним счётчиком, а набором ячеек, чтобы не создавать точку конкуренции.
Дальше стоит назвать практические отличия:
nullне допускается ни ключом, ни значением, потому чтоget, вернувшийnull, было бы не отличить от отсутствия ключа при конкурентном доступе;- итератор слабо согласованный: он не бросает
ConcurrentModificationExceptionи может увидеть часть изменений, сделанных во время обхода; - составные операции атомарны:
putIfAbsent,compute,computeIfAbsent,merge. Именно ими и пользуются, потому что связкаgetиputатомарной не является даже здесь; size()возвращает приблизительное значение, для точного естьmappingCount().
Где спотыкаются
Рассказывают про сегменты и уровень параллелизма. Это устройство до Java 8.
Считают, что Collections.synchronizedMap эквивалентен: он блокирует всю карту целиком и заметно медленнее под нагрузкой.
Пишут if (!map.containsKey(k)) map.put(k, v) и получают гонку, хотя оба вызова потокобезопасны по отдельности.