Почему нельзя использовать обычный Integer для обновления из нескольких потоков
Что проверяют
Понимаете ли вы разницу между атомарностью и видимостью и знаете ли, что обычный инкремент это не одна операция. На Android это не теория: пулы фоновых потоков, корутины и колбэки сети легко приводят к тому, что один счётчик трогают сразу несколько потоков.
Как отвечать
Сам Integer неизменяемый, но поле, которое на него ссылается, меняется, и меняется в три шага: прочитать значение, прибавить единицу, записать обратно. Между чтением и записью встревает другой поток, одно из обновлений теряется, счётчик молча отстаёт.
Вторая беда это видимость. Без volatile или другой границы памяти поток вправе сколь угодно долго читать своё закэшированное значение, а JIT может вообще вынести чтение из цикла. Модель памяти Java не обещает, что запись одного потока когда-нибудь станет видна другому, если между ними нет отношения happens-before.
Отсюда рабочие варианты: AtomicInteger с incrementAndGet (внутри цикл CAS, без блокировки), synchronized или ReentrantLock вокруг всей составной операции, updateAndGet для более сложного пересчёта. В корутинах то же самое решается Mutex либо тем, что состояние живёт в MutableStateFlow и меняется через update.
private val count = AtomicInteger(0)
fun hit(): Int = count.incrementAndGet()Где спотыкаются
Говорят, что достаточно пометить поле volatile. Видимость это чинит, атомарность нет: count++ остаётся тремя действиями, и потерянные обновления никуда не деваются.
Второе: считают, что раз Integer неизменяемый, он потокобезопасен. Неизменяем объект, а ссылка на него перезаписывается, и гоняется именно эта перезапись.
Третье: берут AtomicInteger и снова собирают составную операцию руками: читают get, сравнивают, потом пишут set. Атомарен каждый вызов по отдельности, а не пара, поэтому нужен compareAndSet или updateAndGet.