Как избежать гонок данных при многопоточном доступе к коллекции
Что проверяют
Понимаете ли вы, что потокобезопасная коллекция не делает потокобезопасной вашу логику. Следом почти всегда просят показать, где в коде проверка и действие разъезжаются, и спрашивают, как это решается в корутинах.
Как отвечать
Начните с определения: гонка возникает, когда два потока обращаются к одной изменяемой структуре и хотя бы один пишет без синхронизации. Дальше варианты от простого к сложному:
- один поток на изменения: коллекцию меняют только в одном месте, например на
Dispatchers.Mainили вDispatchers.Default.limitedParallelism(1). Самый дешёвый способ, блокировки не нужны; - неизменяемые данные: храните
ListвStateFlowи заменяйте целиком через_state.update { it + item }. Методupdateатомарный и повторяет попытку при конфликте; ConcurrentHashMap: словарь с конкурентной записью. Составные операции делайте черезputIfAbsent,computeIfAbsent,merge, а не черезgetиputподряд;CopyOnWriteArrayList: для списков, которые читают часто, а меняют редко, например слушатели. Каждая запись копирует массив;Mutexиз kotlinx.coroutines:mutex.withLock { }приостанавливает корутину, а не блокирует поток. Он нереентерабельный: повторныйlockиз той же корутины даст дедлок;synchronizedиReentrantLock: для обычных потоков. Вызывать suspend-функции внутриsynchronizedкомпилятор Kotlin не даст.
Закончите выбором: сначала попробуйте убрать разделяемое изменяемое состояние, и только потом ставьте блокировки.
Где спотыкаются
Оборачивают список в Collections.synchronizedList и перебирают его без внешнего synchronized(list). Итерация не атомарна, и рано или поздно прилетает ConcurrentModificationException.
Пишут if (!map.containsKey(key)) map.put(key, value) на ConcurrentHashMap и считают код безопасным. Каждый вызов атомарен, пара нет: между ними успевает вклиниться другой поток.
Кладут MutableList в StateFlow и меняют его на месте. Ссылка та же, StateFlow видит равное значение и ничего не рассылает, а список тем временем меняется под читающими его потоками.