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

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

Открыта

— Привет, Амиго! Помнишь, Элли тебе рассказывала про проблемы при одновременном доступе нескольких нитей к общему (разделяемому) ресурсу?

— Да.

— Так вот – это еще не все. Есть еще небольшая проблема.

Как ты знаешь, в компьютере есть память, где хранятся данные и команды (код), а также процессор, который исполняет эти команды и работает с данными. Процессор считывает данные из памяти, изменяет и записывает их обратно в память. Чтобы ускорить работу процессора в него встроили свою «быструю» память – кэш.

Чтобы ускорить свою работу, процессор копирует самые часто используемые переменные из области памяти в свой кэш и все изменения с ними производит в этой быстрой памяти. А после – копирует обратно в «медленную» память. Медленная память все это время содержит старые(!) (неизмененные) значения переменных.

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

— И все?

— Да. Просто и красиво.

Комментарии (283)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
10 января, 01:00
Exaltyr777
Уровень 25
27 сентября 2025, 16:12
А я то думал что volatile это то же что и synchronized, ток для переменных. И кстати зачем в примере значение переменной isCancel у одной нити вообще нужно другой нити? У каждой нити есть ведь своя переменная isCancel в своём объекте Thread =\
C0N5P1RACY
Уровень 38
4 февраля, 19:26
Тоже не совсем понял этот момент, вроде же создается отдельная переменная для каждой нити.. А вроде и нет... Или поля типо общие.. Но логично будет предположить что они у каждой нити свои... как много вопросов, как мало ответов...
Anonymous #3585174
Уровень 33
27 августа 2025, 11:18
Like
kekich119Beginner Java Develoeper в at Home
23 июня 2025, 13:37
очень классная задачка "Аптека", особенно когда вчитываешься в код
Alexander
Уровень 1
16 мая 2025, 05:52
Ключевое слово volatile в Java используется для переменных, которые могут использоваться в многопоточном окружении, и его основная цель — обеспечить видимость изменений переменной между потоками. 📌 Что делает volatile? Когда переменная объявлена как volatile, это означает две вещи: 1. Видимость (Visibility): Если один поток изменяет значение переменной volatile, то другие потоки сразу увидят это новое значение. Без volatile поток может использовать устаревшее значение из своего кэша. 2. Запрет на кэширование и оптимизации: Компилятор и процессор не будут делать оптимизации, которые могут привести к тому, что поток будет видеть устаревшее значение переменной. 🧠 Без volatile может произойти так:
class Example {
    boolean running = true;

    void run() {
        while (running) {
            // что-то делаем
        }
    }

    void stop() {
        running = false;
    }
}
Если метод run() выполняется в одном потоке, а stop() вызывается в другом, поток run() может никогда не увидеть, что running стало false, потому что он может читать значение из кэша. ✅ А вот так — безопасно:
class Example {
    volatile boolean running = true;

    void run() {
        while (running) {
            // что-то делаем
        }
    }

    void stop() {
        running = false;
    }
}
Теперь, как только stop() изменит переменную, поток в run() обязательно увидит это изменение.
Alexander
Уровень 1
16 мая 2025, 05:53
❗️Важно: volatile не делает операции атомарными (например, count++ не станет потокобезопасной только потому, что count — volatile). Для сложных операций, где нужно синхронизировать несколько действий, следует использовать synchronized, ReentrantLock или атомарные классы из java.util.concurrent.atomic. 📌 Пример с числом:
class Counter {
    volatile int count = 0;

    void increment() {
        count++;  // ❌ НЕ безопасно, даже с volatile!
    }
}
Хотя count объявлен как volatile, операция count++ всё равно не потокобезопасна, потому что она состоит из: 1. Чтения count. 2. Увеличения на 1. 3. Записи нового значения обратно. На этих этапах возможны конфликты между потоками.
Артём ПъДиректор в Самокат
2 февраля 2025, 05:14
Скрипим, но едем дальше +++++++
{Java_Shark}
Уровень 36
30 октября 2024, 12:07
непонятно... но интересно, думаю дальше разберемся))) всем добра++
Philipp Vasichev
Уровень 28
10 октября 2024, 11:18
🤫
greatov
Уровень 51
3 августа 2024, 09:27
я не совсем понял, если помечаем переменную volatile, и в середине процесса вычисления/измения переменной к ней обратится другая нить она будет ждать окончания вычисления или возмет то что есть в медленной памяти? Дополнение! Модификатор volatile накладывает некоторые дополнительные условия на чтение/запись переменной. Важно понять две вещи о volatile переменных: Операции чтения/записи volatile переменной являются атомарными . Результат операции записи значения в volatile переменную одним потоком, становится виден всем другим потокам, которые используют эту переменную для чтения из нее значения.
25 февраля 2025, 08:18
volatile не дает атомарности. если в потоке использовать volatile инкремент, то volatile не спасет и лучше использовать synchronized
Dr.Alexcemen9Врач в Поликлиника в деревн
15 июня 2024, 15:41
"Сначала думали отключить работу с кэшем, но потом оказалось, что из-за этого программы работают в разы медленнее." Надо было просто создать автономную стрелку мышки, которая бы обводила восьмерки на экране. И проблема с медленной работой программы была бы решена. Когда программа тормозит - просто водишь мышкой восьмерку, и все зарабатывает. Вот и тут над сделать что-то похожее👌