Почему нельзя пометить все методы synchronized для решения race condition
Что проверяют
Понимаете ли вы, что гонка это свойство инварианта, а не отдельного метода, и осознаёте ли, чем блокировка обходится приложению с главным потоком.
Как отвечать
synchronized на методе экземпляра берёт монитор this, на статическом методе монитор класса. Пометить всё подряд не решение сразу по нескольким причинам.
Гонка живёт между вызовами. Если один метод проверяет наличие ключа, а другой кладёт значение, оба синхронизированы по отдельности, а проверка и запись всё равно разъезжаются. Защищать нужно составную операцию целиком, а не каждый её шаг.
Состояние утекает. Синхронизированный геттер, который отдаёт наружу изменяемый список, отдаёт его без всякой защиты, и дальше с ним работают из любого потока.
И главное для Android: блокировка выстраивает всех в очередь, включая главный поток. Фоновый поток зашёл в длинный метод, интерфейс дёрнул синхронизированный геттер при отрисовке списка и встал, через несколько секунд пользователь видит ANR. Растёт и риск взаимной блокировки: синхронизированный метод, который вызывает чужой синхронизированный метод, легко замыкает цикл.
Что вместо: сузить критическую секцию до нескольких строк, сделать состояние неизменяемым и заменять его целиком, взять ConcurrentHashMap или AtomicReference.updateAndGet, а в корутинах держать состояние в MutableStateFlow и менять через update.
Где спотыкаются
Считают, что после расстановки synchronized везде код потокобезопасен. Безопасен каждый метод по отдельности, а инвариант из двух вызовов рвётся по-прежнему, и такой баг воспроизводится раз в неделю и только у пользователей.
Второе: забывают про главный поток. На сервере лишняя блокировка это потерянная пропускная способность, на устройстве это застывший интерфейс и отчёт об ANR, то есть прямо видимый дефект.
Третье: тащат synchronized в корутины. Внутри него нельзя приостановиться, а блокирующее ожидание занимает поток диспетчера целиком. Для приостанавливаемого кода берут Mutex и withLock.