Как битность примитивов влияет на многопоточность в Java
Что проверяют
Знаете ли вы модель памяти Java настолько, чтобы отличать атомарность записи от атомарности операции. Ключевой момент здесь один: 64-битные long и double. Следом обычно спрашивают, почему volatile не спасает counter++.
Как отвечать
- чтение и запись: по спецификации (JLS, раздел 17.7) чтение и запись
int,short,byte,char,boolean,floatи ссылок атомарны. Запись в не-volatileполеlongилиdoubleJVM вправе выполнить как две 32-битные половины, и другой поток может прочитать значение, склеенное из двух разных записей (word tearing). Модификаторvolatileделает чтение и записьlongиdoubleатомарными; - 64-битные устройства: на 64-битных JVM и ART такие записи на практике атомарны, но спецификация этого не гарантирует. Корректный код опирается на модель памяти, а не на процессор;
- атомарность операции:
count++это чтение, сложение и запись, и никакая битность этого не меняет. НужныAtomicInteger,AtomicLong,LongAdderдля счётчиков под высокой конкуренцией или синхронизация; - видимость:
volatileзадаёт отношение happens-before между записью и последующим чтением, без него поток может долго не видеть изменения. В Kotlin то же самое делает@Volatile; - соседние поля: запись в
byteне затирает соседнийbyte, это тоже гарантия спецификации. Но поля, которые разные потоки активно пишут и которые лежат в одной строке кеша (обычно 64 байта), дают false sharing и падение производительности без ошибок в логике.
В Android это всплывает, когда long-таймстемп или счётчик пишет фоновый поток, а читает главный.
Где спотыкаются
Говорят, что операции с int атомарны, имея в виду инкремент. Атомарны только отдельные чтение и запись.
Объясняют volatile как «читать из основной памяти». Правильнее говорить про happens-before и запрет переупорядочивания и помнить, что составные действия он не защищает.
Утверждают, что на 64-битном телефоне проблемы нет, поэтому volatile у long не нужен. Гарантии атомарности спецификация не даёт, а без volatile нет и видимости.