— Привет, Амиго!

— Привет, Элли!

— Хочу рассказать тебе о модификаторе volatile. Знаешь, что это такое?

— Вроде что-то связанное с нитями. Не помню точно.

— Тогда слушай. Вот тебе немного технических деталей:

В компьютере есть два вида памяти – глобальная (обычная) и встроенная в процессор. Встроенная в процессор делится на регистры, затем кэш первого уровня (L1), кэш второго уровня (L2) и третьего уровня (L3).

Эти виды памяти отличаются по скорости работы. Самая быстрая и самая маленькая память – это регистры, затем идет кэш процессора (L1, L2, L3) и, наконец, глобальная память (самая медленная).

Скорость работы глобальной памяти и кэша процессора сильно отличаются, поэтому Java-машина позволяет каждой нити хранить самые часто используемые переменные в локальной памяти нити (в кэше процессора).

— А можно как-то управлять этим процессом?

— Практически никак – всю работу делает Java-машина, она очень интеллектуальная в плане оптимизации скорости работы.

Но я тебе это рассказываю вот зачем. Есть одна маленькая проблемка. Когда две нити работают с одной и той же переменной, каждая из них может сохранить ее копию в своем внутреннем локальном кэше. И тогда может получится такая ситуация, что одна нить переменную меняет, а вторая не видит этого изменения, т.к. по-прежнему работает со своей копией переменной.

— И что же делать?

— На этот случай разработчики Java предусмотрели специальное ключевое слово – volatile. Если есть переменная, к которой обращаются из разных нитей, ее нужно пометить модификатором volatile, чтобы Java-машина не помещала ее в кэш. Вот как это обычно выглядит:

public volatile int count = 0;

— О, вспомнил. Ты же уже про это рассказывала. Я же это уже знаю.

— Ага, знаешь. А вспомнил, только когда я рассказала.

— Э, ну забыл немного.

— Повторение – мать учения.

Вот тебе несколько новых фактов работы модификатора volatile. Модификатор volatile гарантирует только безопасное чтение/запись переменной, но не ее изменение.

— А в чем разница?

— Вот смотри. Как изменяется переменная:

Код Что происходит на самом деле: Описание
count++
register = count;

register = register+1;

count = register;
Этап 1.
Значение переменной count копируется из глобальной памяти в регистровую память процессора.

Этап 2
Внутри процессора регистровая переменная увеличивается на 1.

Этап 3
Значение переменной копируется из процессора в глобальную память.

— Ого! Так что, изменение любой переменной происходит только в процессоре?

— Ага.

— И значения копируются туда-сюда: из памяти в процессор и обратно?

— Ага.

Так вот, модификатор volatile, гарантирует, что при обращении к переменной count она будет прочитана из памяти (этап 1). А если какая-то нить захочет присвоить ей новое значение, то оно обязательно окажется в глобальной памяти (этап 3).

Но Java-машина не гарантирует, что не будет переключения нитей между этапами 1 и 3.

— Т.е. увеличение переменной на 1 – это фактически три операции?

— Да.

— И если две нити одновременно захотят исполнить count++, то они могут помешать друг другу?

— Да, вот смотри:

Нить 1 Нить 2 Результат
register1 = count;
register1++;
count = register1;
register2 = count;
register2++;
count = register2;
register1 = count;
register2 = count;
register2++;
count = register2;
register1++;
count = register1;

— Т.е. обращаться к переменной можно, а изменять рискованно все равно?

— Ну, изменять можно, только осторожно ☺

— Это как же?

— synchronized наше все.

— Ясно.