JavaRush /Курси /Java Core /Проблема багатопотоковості — локальний кеш. Volatile

Проблема багатопотоковості — локальний кеш. Volatile

Java Core
Рівень 7 , Лекція 5
Відкрита

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

— Так.

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

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

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

І тоді може виникнути проблема. Один потік змінює змінну, таку як 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();
}

— І все?

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

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ