Как объяснить работу переменной, используемой несколькими потоками без синхронизации
Что проверяют
Знаете ли вы модель памяти Java, а не только слово «гонка». Главное здесь понимать, что без синхронизации ломаются три вещи: атомарность, видимость и порядок операций. Следом обычно спрашивают, чем volatile отличается от synchronized.
Как отвечать
Разложите проблему на три части:
- атомарность:
count++это чтение, увеличение и запись. Два потока читают одно значение, и одно увеличение теряется. Записьlongиdoubleбезvolatileпо спецификации может быть даже не атомарной; - видимость: поток может не увидеть запись другого потока. JIT имеет право вынести чтение обычного поля из цикла, и
while (!stopped)превращается в бесконечный цикл; - переупорядочивание: компилятор и процессор меняют порядок операций, поэтому другой поток может увидеть ссылку на объект раньше, чем его поля заполнены. Классический пример это double-checked locking без
volatile.
Затем назовите правило: корректность гарантирует только отношение happens-before. Его создают выход из synchronized и вход в него на том же мониторе, запись и чтение volatile, Thread.start() и join(), классы из java.util.concurrent.
Инструменты по задаче:
volatile: флаги и безопасная публикация ссылки, видимость и порядок без атомарности составных операций;AtomicInteger,LongAdder: счётчики,LongAdderлучше при высокой конкуренции;synchronized,ReentrantLock: инварианты из нескольких полей;- неизменяемые объекты с
final-полями: после конструктора их можно читать из любых потоков.
Виртуальные потоки из Java 21 модель памяти не меняют.
Где спотыкаются
Говорят только о потерянных инкрементах и забывают про видимость, хотя именно она даёт зависания, которые не воспроизводятся в отладчике.
Считают, что volatile решает проблему счётчика. volatile int с ++ по-прежнему теряет обновления.
Синхронизируют только запись, а читают без блокировки. Читающий поток тоже должен синхронизироваться на том же мониторе, иначе гарантий видимости нет. Отсутствие ошибок в тестах на x86 ничего не доказывает.