Как два потока с чередующимися записями/чтениями могут напечатать [0, 0]?
Два потока каждый пишут одно поле, затем читают другое. Объясните, как [0, 0] возможен без @Volatile, что исключает @Volatile и что всё ещё не гарантировано.
class State { var x = 0; var y = 0 }
// t1: state.x = 1; println(state.y)
// t2: state.y = 1; println(state.x)
Определите причину вывода [0, 0].
Запись каждого потока может быть не видна другому, а переупорядочивание пускает оба чтения до закрепления записей, — поэтому [0, 0] легален. @Volatile на x и y добавляет рёбра happens-before, исключающие [0, 0], но [1, 1] требует явной синхронизации или join.
- ✗Считать, что записи в порядке программы всегда мгновенно видны другим потокам
- ✗Думать, что
@Volatileгарантирует результат[1, 1], а не только исключает[0, 0] - ✗Полагать, что одиночным записям нужна атомарность, а не видимость/порядок
- →Почему
@Volatileисключает[0, 0], но не форсирует[1, 1]? - →Как
joinперед чтениями гарантировал бы[1, 1]?
Баг (поведение)
Без барьеров памяти модель памяти Java (JMM) разрешает:
- Невидимость записи — запись
x = 1в потоке t1 может быть ещё не видна t2. - Переупорядочивание — компилятор/процессор может переставить независимые запись и чтение.
class State { var x = 0; var y = 0 }
// t1: state.x = 1; println(state.y) // может прочитать y = 0
// t2: state.y = 1; println(state.x) // может прочитать x = 0
Поэтому возможны все четыре исхода, включая [0, 0]: оба чтения проходят до того, как чужая запись стала видна.
Исправление (сужение)
Пометьте x и y как @Volatile. Это устанавливает happens-before: чтение, видящее старт, не может пропустить предшествующую запись — [0, 0] исключается.
class State { @Volatile var x = 0; @Volatile var y = 0 }
Но [1, 1] по-прежнему не гарантирован — он зависит от тайминга. Чтобы получить именно [1, 1], нужна явная синхронизация (например, join потоков перед чтением или общий барьер).