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

— Привіт, Еллі!

— Хочу розповісти тобі про модифікатора 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 наше все.

— Ясно.