Как определить, что класс не потокобезопасен
Что проверяют
Умеете ли вы найти проблему чтением кода, а не только после краша. Интервьюер ждёт конкретных признаков и почти всегда просит тут же поправить показанный класс или объяснить, почему @Volatile его не спасёт.
Как отвечать
Первый вопрос: есть ли у класса изменяемое состояние, к которому обращаются из разных потоков. Если состояния нет или доступ строго с одного потока, например только с главного, проблем нет. Дальше признаки:
- составные операции без блокировки:
count++, «проверить и сделать» вродеif (!map.containsKey(k)) map.put(k, v), ленивая инициализация черезif (instance == null). По отдельности каждая строка безобидна, вместе это гонка; - видимость: флаг
isRunning, который один поток пишет, а другой читает в цикле, без@Volatileили атомика. Второй поток может так и не увидеть изменение; - изменяемые коллекции наружу:
ArrayList,HashMap,mutableListOf()в общем поле или в геттере.ConcurrentModificationExceptionпри обходе частый симптом; - утечка
thisиз конструктора: слушатель регистрируется до окончания инициализации, и другой поток видит полусобранный объект; - известные небезопасные классы: общий статический
SimpleDateFormat, замена ему неизменяемыйDateTimeFormatter.
Смотрите и на аннотации: @MainThread и @WorkerThread из androidx.annotation показывают ожидания автора, а @GuardedBy вместе с Error Prone проверяет, что поле читается под нужной блокировкой. В корутинах решение это Mutex из kotlinx.coroutines, MutableStateFlow.update {} или один поток через limitedParallelism(1).
Где спотыкаются
Считают, что @Volatile делает класс потокобезопасным. Он даёт видимость, но count++ над таким полем остаётся гонкой.
Проверяют только запуском: «запускал много раз, всё работало». Гонка проявляется редко и на конкретных устройствах, для JVM есть стресс-тесты вроде jcstress, но главное средство всё равно чтение кода.
Синхронизируются на разных объектах: в одном методе на this, в другом на отдельном локе. Формально synchronized стоит везде, а взаимного исключения нет.