Что лучше использовать: synchronized блоки или атомарные операции и почему
Что проверяют
Понимаете ли вы разницу между атомарностью одной ячейки и согласованностью нескольких, знаете ли цену блокировки на ART и помните ли, что на Android ожидание в главном потоке превращается в ANR.
Как отвечать
Правило простое: атомики для одной переменной, монитор для инварианта из нескольких.
AtomicInteger,AtomicLong,AtomicReferenceработают через CAS в цикле и не паркуют поток.counter.incrementAndGet()атомарен, аvolatile intс++нет: это три отдельных действия, чтение, сложение, запись;synchronized(в Kotlin@Synchronizedилиsynchronized(lock) { }) нужен, когда под защитой несколько полей или составная операция вида «проверил и записал», и когда важно, чтобы группа изменений стала видна целиком;- цена: монитор без конкуренции на ART дёшев, тонкая блокировка снимается почти даром. При реальной конкуренции он раздувается, и поток уходит в планировщик. Атомик при высокой конкуренции тоже не бесплатен, CAS крутится в цикле и жжёт процессор, для счётчиков лучше
LongAdder; - в корутинах вместо монитора берут
Mutexизkotlinx.coroutines: внутриsynchronizedнельзя приостановиться, и он держит поток целиком.
Где спотыкаются
Собирают инвариант из атомиков. Два AtomicInteger атомарны по отдельности, а пара из них уже нет, и конструкция «прочитал значение, сравнил, записал» остаётся гонкой. Нужен compareAndSet либо одно состояние целиком под AtomicReference.
Второе: синхронизируются по изменяемой ссылке или по this публичного класса. В первом случае монитор меняется вместе с полем и потоки встают на разных объектах, во втором чужой код может захватить ту же блокировку и устроить взаимную.
Третье: держат блокировку в главном потоке. Ожидание монитора в потоке интерфейса это пропущенные кадры и в пределе ANR, поэтому под блокировкой не делают ни ввод-вывод, ни разбор данных, а выносят их наружу.