Можем ли столкнуться с проблемами при работе с double в многопоточном окружении в Java
Что проверяют
Знаете ли вы модель памяти Java в части атомарности 64-битных типов и видимости, а не только общую фразу про гонки. Следом обычно спрашивают, спасает ли volatile операцию x += 1.0 и есть ли в JDK AtomicDouble.
Как отвечать
Проблем три, их стоит разделить:
- атомарность записи: по JLS 17.7 запись в неволатильные
longиdoubleможет выполняться двумя 32-битными половинами, и другой поток прочитает смесь старого и нового значения. На 64-битных JVM так на практике не бывает, но спецификация этого не гарантирует. Дляvolatile-полей чтение и запись атомарны всегда; - видимость: без
volatile,synchronizedили другого отношения happens-before поток может долго видеть старое значение, например если JIT вынес чтение из цикла; - составные операции:
x += 1.0это чтение, сложение и запись.volatileне делает их одной атомарной операцией, и при параллельных инкрементах обновления теряются.
Решения:
synchronizedилиReentrantLock: когда вместе с числом меняется что-то ещё;- CAS на битах:
AtomicLongплюсDouble.doubleToRawLongBits()иDouble.longBitsToDouble()в циклеcompareAndSet. ГотовогоAtomicDoubleв JDK нет, в Guava он построен на том же приёме; DoubleAdderиDoubleAccumulator: изjava.util.concurrent.atomic, для сумм и метрик под высокой конкуренцией. Потоки пишут в разные ячейки,sum()складывает их при чтении;- неизменяемые объекты: публикация готового значения через
volatile-ссылку.
Про точность: сложение double неассоциативно, поэтому сумма через DoubleAdder или параллельный стрим может отличаться в последних знаках от прогона к прогону, это оговорено в документации. Деньги в double не считают вовсе: BigDecimal или целые копейки в long.
Где спотыкаются
Делают счётчик на volatile double и считают задачу решённой. Инкременты всё равно теряются.
Не знают, что неатомарность long и double прописана в спецификации, и ссылаются на «у меня всё работает». Код опирается на гарантии, а не на поведение конкретной JVM.
Путают многопоточность с точностью: 0.1 + 0.2 != 0.3 будет и в одном потоке, синхронизация здесь ни при чём.