В «порядке исходного кода» увеличение до i
происходит до увеличение до j
. Таким образом, метод вызова потока one()
всегда будет наблюдать, что i >= j
. Но когда другие потоки видят эти переменные, они могут видеть вещи по-другому.
Существуют определенные события, которые устанавливают то, что JLS называет «порядком синхронизации». Имеет смысл говорить об этих событиях (и только этих событиях) как о "происходящих раньше" других. Запись в переменную volatile является одним из них. Без использования volatile
бессмысленно говорить, что i
увеличивается до j
; эти записи могут быть переупорядочены, и этот порядок может наблюдаться другими потоками.
Лучший пример того, что может произойти без volatile
, будет следующим:
static void oneAndAHalf() { System.out.println("j=" + j + " i=" + i);
Даже если j
появляется для увеличения после i
и j
выбирается до i
, вы все равно можете наблюдать j > i
, поскольку удаление volatile
позволит переупорядочить операции в one()
. Добавьте volatile
, и oneAndAHalf()
всегда будет отображать i >= j
, как вы ожидаете.
Если вы уберете volatile
, то метод two()
может напечатать значение для j
, которое больше i
по одной из двух причин: поскольку операции были переупорядочены, или потому что i
и j
не рассматриваются атомарно. Текущий метод two()
не однозначно иллюстрирует полезность volatile
. Добавьте volatile
, и вы получите тот же результат, но единственная причина в том, что операции не являются атомарными.
Чтобы увидеть согласованное представление, где i == j
, оба метода могут быть synchronized
. Это приведет к тому, что приращение к обеим переменным будет атомарным.