Какие состояния гонки и неожиданные результаты могут возникнуть при использовании статических переменных в многопоточном Java коде
Что проверяют
Понимаете ли вы, что статическое поле одно на класс в рамках загрузчика и общее для всех потоков, и знаете ли модель памяти Java. Следом обычно просят объяснить, почему volatile не спасает счётчик, и написать потокобезопасный ленивый синглтон.
Как отвечать
Назовите конкретные классы проблем:
- потерянное обновление:
counter++это три шага: чтение, сложение, запись. Два потока читают одно значение, и одно увеличение теряется. НуженAtomicInteger,LongAdderпри высокой конкуренции илиsynchronized; - видимость: без отношения happens-before поток может не увидеть чужую запись. Классика: цикл
while (!stopped)по статическомуbooleanкрутится вечно, потому что JIT вынес чтение из цикла. Лечитсяvolatile; - проверка, затем действие: ленивая инициализация
if (instance == null) instance = new Service()создаёт два объекта. Double-checked locking корректен только сvolatile, иначе другой поток увидит ссылку на недостроенный объект. Проще holder-идиома илиenum: инициализация класса в JVM потокобезопасна; - непотокобезопасные объекты в статических полях:
static SimpleDateFormatвыдаёт мусорные даты,static HashMapтеряет записи и портит внутреннюю структуру. Замена: неизменяемыйDateTimeFormatterиConcurrentHashMap; - разрыв 64-битных значений: запись в
longиdoubleбезvolatileпо спецификации может быть неатомарной, и поток прочитает половину старого и половину нового значения.
Закончите способами защиты: неизменяемость, java.util.concurrent, ThreadLocal для объектов на поток. И тем, как такие баги ловят: стресс-тесты вроде jcstress, потому что обычный прогон воспроизводит гонку редко.
Где спотыкаются
Считают, что volatile делает ++ атомарным. Он даёт видимость и порядок, но не атомарность составной операции.
Смешивают static synchronized и обычный synchronized метод. Первый блокирует объект Class, второй this, и одно статическое поле оказывается под двумя разными мониторами, то есть не защищено вовсе.
Забывают чистить ThreadLocal в пуле потоков. Поток переиспользуется, и следующий запрос получает чужие данные, поэтому в finally нужен remove().