Что происходит при одновременном изменении объекта двумя потоками без синхронизации
Что проверяют
Понимаете ли вы, что у гонки две стороны: потерянные обновления и невидимые записи. Вторая часть, видимость, отсеивает большинство: про неё вспоминают реже, а ловится она хуже.
Как отвечать
Без синхронизации гарантий нет никаких, и это точная формулировка, а не фигура речи.
- потеря обновлений:
count++это чтение, увеличение и запись. Два потока прочитают одно значение, и одно из увеличений пропадёт; - видимость: без отношения предшествования второй поток может не увидеть запись вообще и сколь угодно долго читать старое значение, лежащее в регистре или в кэше ядра;
- переупорядочивание: компилятор и процессор вправе менять порядок операций, поэтому ссылка на объект может стать видна другому потоку раньше, чем допишутся его поля;
- неатомарность записи: запись
longиdoubleбезvolatileразрешено выполнять двумя частями, и прочитать можно половину от одного значения и половину от другого.
Дальше инструменты: volatile чинит видимость, но не делает составную операцию атомарной; synchronized и ReentrantLock дают и взаимное исключение, и видимость; AtomicInteger и родственники закрывают счётчики; коллекции из java.util.concurrent заменяют самодельную синхронизацию. Самое надёжное остаётся прежним: неизменяемые объекты и состояние, которым владеет один поток.
Где спотыкаются
Чинят гонку одним volatile на поле счётчика. Видимость это лечит, атомарность ++ нет, дефект остаётся на месте и начинает воспроизводиться ещё реже. Для счётчика нужен атомарный тип или блокировка.
Второе: ждут от гонки внятного симптома. Исключение о конкурентном изменении коллекции это проверка на лучших усилиях: она может не сработать ни разу, и её отсутствие ничего не доказывает. Проявится гонка у пользователя на другом устройстве, а в отчёте будет выглядеть как случайно неверное число на экране.
Третье: в Android считают потокобезопасный держатель состояния защитой от гонки. Присваивание значения действительно потокобезопасно, но чтение с последующей записью из двух корутин это составная операция, и нужен атомарный вариант обновления. Отдельно помните, что вью трогается только с главного потока, и синхронизацией это не обходится.