Как работает ConcurrentHashMap
Что проверяют
Знаете ли вы, как устроен ConcurrentHashMap сейчас, а не в Java 7, и умеете ли применить его в Android-коде. Следом обычно спрашивают, чем он лучше Collections.synchronizedMap и атомарен ли getOrPut в Kotlin.
Как отвечать
Устройство начиная с Java 8 (в современных версиях Android та же реализация):
- таблица бакетов: массив узлов, как у
HashMap. Сегментов больше нет, классSegmentостался только ради совместимости сериализации; - чтение без блокировок:
getчитает бакет volatile-чтением и не ждёт писателей, поэтому видит последнее завершённое изменение; - запись: в пустой бакет узел кладётся через CAS, в непустой берётся
synchronizedна первом узле бакета. Потоки, пишущие в разные бакеты, друг другу не мешают; - деревья: когда цепочка дорастает до 8 элементов при таблице от 64 бакетов, она превращается в красно-чёрное дерево, а при меньшей таблице карта сначала расширяется;
- ресайз: идёт по частям, и потоки, которые на него наткнулись, помогают перенос закончить;
- правила:
nullв ключах и значениях запрещён, итераторы слабо согласованные и не бросаютConcurrentModificationException,size()под нагрузкой приблизительный.
Главное на практике: атомарны отдельные операции, а не их комбинации. Для «проверить и записать» есть putIfAbsent, computeIfAbsent, merge.
В Android такая карта уместна для кеша в репозитории или синглтоне, к которому ходят из нескольких потоков Dispatchers.IO. Если состояние читает UI, часто удобнее MutableStateFlow с update { }, а сложную логику над общим состоянием проще закрыть Mutex из корутин.
Где спотыкаются
Рассказывают про 16 сегментов и ReentrantLock на каждом. Так было до Java 8, и интервьюер воспримет это как устаревшее знание.
Считают Kotlin-функцию getOrPut атомарной. Для ConcurrentMap она не перезапишет существующее значение, но лямбда может вызваться лишний раз. Если создание дорогое, нужен computeIfAbsent.
Делают долгую работу или сетевой запрос внутри computeIfAbsent. Лямбда выполняется под блокировкой бакета и тормозит других писателей, а изменение той же карты изнутри может закончиться исключением или зависанием.