— Привіт, Аміго! Пам'ятаєш, Еллі розповідала тобі про проблеми під час одночасного доступу кількох потоків до загального (того, що поділяється) ресурсу?

— Так.

— Так ось – це ще не все. Є ще невеличка проблема.

Як ти знаєш, у комп'ютера є пам'ять, де зберігаються данні і команди (код), а також процесор, який виконує ці команди і працює з данними. Процесор зчитує данні з пам'яті, змінює і записує їх назад у пам'ять. Щоб прискорити роботу процесора, в нього вбудували «швидку» пам'ять – кеш.

Щоб прискорити свою роботу, процесор копіює найбільш використовувані змінні з області пам'яті у свій кеш, і всі зміни з ними здійснює у цій швидкій пам'яті. А після – копіює назад у «повільну» пам'ять. Повільна пам'ять весь цей час містить старі (!) (тобто не змінені) значення змінних.

І тоді може виникнути проблема. Один потік змінює змінну, таку як isCancel або isInterrupted з прикладу нижче, а другий потік «не бачить» цієї зміни, оскільки вона була виконана у швидкій пам'яті. Це наслідок того, що потоки не мають доступу до кешу один одного. Процесор часто містить кілька незалежних ядер і потоки фізично можуть виконуватись на різних ядрах.

Згадаємо учорашній приклад:

Код Опис
class Clock implements Runnable
{
private boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tik");
}
}
}
Потік «не знає» про існування інших потоків.

У методі run змінна isCancel під час першого використання буде поміщена до кешу дочірнього потоку. Ця операція еквівалентна коду:

public void run()
{
boolean isCancelCached = this.isCancel;
while (!isCancelCached)
{
Thread.sleep(1000);
System.out.println("Tik");
}
}

Виклик методу cancel з іншого потоку змінить значення змінної isCancel у звичайній (повільній) пам'яті, але не в кеші інших потоків.

public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

— Вау! А для цієї проблеми теж придумали гарне рішення, як у випадку з synchronized?

— Ти не повіриш!

Спочатку думали відключити роботу з кешем, але потім виявилося, що через це програми працюють у рази повільніше. Тоді вигадали інше рішення.

Було придумано спеціальне ключове слово volatile. Роміщення цього слова перед визначенням змінної забороняло поміщати її значення у кеш. Точніше, не забороняло поміщати у кеш, а просто примусово завжди читало і писало її лише у звичайну (повільну) пам'ять.

Ось як треба виправити наше рішення, щоб все запрацювало як треба:

Код Опис
class Clock implements Runnable
{
private volatile boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tik");
}
}
}
Через модифікатор volatile читання та запис значення змінної завжди відбуватимуться у звичайній, спільній для всіх потоків, пам'яті.
public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

— І все?

— Так. Просто і гарно.