Переменные volatile

Последнее обновление: 22.10.2025

Ключевое слово volatile предлагает механизм синхронизации доступа к полю экземпляра без блокировки. Зачем оно нужно? При выполнении потоки могут кэшировать значения переменных, а компилятор и процессор — переупорядочивать инструкции для оптимизации. Ключевое слово volatile является одним из инструментов для решения этих проблем. При объявлении поля с ключевым словом volatile, компилятор и виртуальная машина учитывают, что поле может одновременно обновляться другим потоком. При компиляции компилятор вставит соответствующий код, чтобы гарантировать, что изменение volatile-переменной в одном потоке будет видно из любого другого потока, который считывает эту переменную.

Основная проблема, которую призвано решить слово volatile, - это проблема видимости. В соответствии с моделью памяти Java (Java Memory Model или JMM), каждый поток имеет свою собственную "рабочую память" (work memory), которая часто реализуется как кэш процессора. Когда поток изменяет значение переменной, он сначала может изменить его в своем локальном кэше. Другие потоки могут не увидеть это изменение немедленно, так как они могут читать "устаревшее" значение из основной памяти или из "своих" кэшей.

Рассмотрим простой пример:

public class Program {
 
    public static void main(String[] args) throws InterruptedException{
         
        Worker worker = new Worker();
        
        // Создаем "Поток А" и запускаем его. Он вызовет worker.run()
        Thread workerThread = new Thread(worker);
        workerThread.start(); // <-- Здесь запускается run()

        // Даем "Потоку А" немного времени, чтобы он вошел в цикл while(true)
        Thread.sleep(1000); // Ждем 1 секунду

        // "Поток Б" (главный поток) вызывает stop()
        worker.stop(); // <-- Здесь вызывается stop()

        // Ждем завершения "Потока А"
        // Если флаг running не был volatile, join() может ждать вечно!
        workerThread.join(2000); // Ждем до 2 секунд

        if (workerThread.isAlive()) {
            System.out.println("--- РЕЗУЛЬТАТ: Поток А все еще работает! ---");
            System.out.println("Поток А не увидел изменение running = false.");
            System.exit(1); // Завершаем программу, так как поток "завис"
        } else {
            System.out.println("--- РЕЗУЛЬТАТ: Поток А  успешно завершился. ---");
        }
    }
}

class Worker implements Runnable {

    // Флаг НЕ volatile
    private boolean running = true;

    @Override
    public void run() {
        
        // Этот код выполняется в "Потоке А"
        System.out.println("Поток А: Начинаю работу...");
        while (running) {
            // Здесь может быть некоторая работа
            
            // JIT-компилятор с высокой вероятностью "закэширует" переменную running
            // в регистре процессора, так как она не меняется внутри цикла.
        }
        // Эта строка может никогда не выполниться
        System.out.println("Поток А: Работа остановлена.");
    }

    public void stop() {
        // Этот код выполняется в "Потоке Б" (главном потоке)
        System.out.println("Поток Б: Отправляю команду на остановку...");
        this.running = false;
    }
}

Здесь определен класс Worker, который представляет реализацию интерфейса Runnable - задачу, которая может выполняться в потоках. В этом классе определено два метода. Метод run() - это как раз реализация Runnable. Этот метод будет выполняться в дочернем потоке (поток А), пока переменная running равна true. А второй метод - stop() предназначен для сброса переменной running в false в главном потоке (поток Б).

В методе main() создаем объект Worker и запускаем его выполнение в дочернем потоке workerThread (условно "Поток А"):

Worker worker = new Worker();
        
// Создаем "Поток А" и запускаем его. Он вызовет worker.run()
Thread workerThread = new Thread(worker);
workerThread.start(); // <-- Здесь запускается run()

ПРи выполнении потока workerThread фактически выполняется метод run() класса Worker. Так как переменная running равна true, то поток workerThread (Поток А) бесконечно выполняет цикл while

Далее после небольшой задержки в 1 секунду сбрасываем у объекта Worker флаг running в false (чтобы Поток А завершил выполнение) и с помощью вызова метода join() ждем завершения потока workerThread (Потока А):

Thread.sleep(1000); // Ждем 1 секунду

// "Поток Б" (главный поток) вызывает stop()
worker.stop();

// Ждем завершения "Потока А"
// Если флаг running не был volatile, join() может ждать вечно!
workerThread.join(2000); // Ждем до 2 секунд

После этого поток workerThread по идее должен завершиться, и мы проверяем его статус с помощью метода isAlive() и выводим соответствующие сообщения на консоль:

if (workerThread.isAlive()) {
    System.out.println("--- РЕЗУЛЬТАТ: Поток А все еще работает! ---");
    System.out.println("Поток А не увидел изменение running = false.");
    System.exit(1); // Завершаем программу, так как поток "завис"
} else {
    System.out.println("--- РЕЗУЛЬТАТ: Поток А  успешно завершился. ---");
}

Но если мы запустим программу, то мы можем получить следующий консольный вывод:

Поток А: Начинаю работу...
Поток Б: Отправляю команду на остановку...
--- РЕЗУЛЬТАТ: Поток А все еще работает! ---
Поток А не увидел изменение running = false.

В чем же проблема? Поток А, скорее всего, оптимизировал свой цикл. Он мог "решить", что раз переменная running не меняется *внутри* цикла, нет смысла каждый раз проверять ее в основной памяти. Этот поток просто использует свое кэшированное значение (true) и бесконечно долбится в бесконечном цикле.

Как это исправить? Изменим в классе Worker одну строку:

private boolean running = true;

на

private volatile boolean running = true;

С volatile Поток А будет вынужден при каждой итерации цикла while проверять значение running в основной памяти. Как только Поток Б запишет туда false, Поток А немедленно это увидит и выйдет из цикла.

Что делает volatile?

Ключевое слово volatile предоставляет две ключевые гарантии:

  • Гарантия Видимости (Visibility)

    volatile — это, по сути, инструкция для JVM: "Никогда не кэшировать эту переменную на уровне потока". И когда переменная объявляется как volatile, происходят следующие моменты:

    • Запись в volatile-переменную всегда производится немедленно в основную память.

    • Чтение volatile-переменной всегда происходит напрямую из основной памяти, аннулируя любое кэшированное значение.

    Так, если в примере выше переменная running была бы объявлена как volatile (volatile boolean running = true), Поток А при каждой проверке while (running) был бы вынужден обращаться к основной памяти. Как только Поток Б записал бы false в основную память, Поток А немедленно бы это увидел и завершил цикл.

  • Гарантия Упорядочивания (Happens-Before)

    volatile предотвращает определенные типы переупорядочивания инструкций, которые могут производить компиляторы и процессоры для повышения производительности. volatile же устанавливает отношение "happens-before":

    • Запись в volatile-переменную происходит после всех предыдущих чтений и записей в коде.

    • Чтение volatile-переменной происходит до (happens-before) всех последующих чтений и записей.

    Это означает, что volatile действует как "барьер памяти". Все, что было до записи в volatile-переменную, гарантированно будет видно любому потоку, который после записи обратился к volatile-переменной.

Атомарность и volatile

Стоит отметить, что volatile не гарантирует атомарность операций. Возьмем классический пример с счетчиком:

private volatile int counter = 0;

public void increment() {
    counter++; // ОПАСНО!
}

Казалось бы, volatile должен помочь, но операция counter++ — это не один шаг. Это три шага:

  1. Чтение: читаем текущее значение counter (из основной памяти, т.к. volatile).

  2. Изменение: увеличиваем это значение (например, с 0 до 1).

  3. Запись: записываем новое значение counter (в основную память, т.к. volatile)

Предположим ситуацию, что два потока (А и Б) вызывают increment() одновременно:

  1. Поток А читает counter (значение 0).

  2. Поток Б читает counter (значение 0).

  3. Поток А изменяет свое значение на 1 и записывает 1 в counter.

  4. Поток Б изменяет "свое" значение (которое все еще 0) на 1 и записывает 1 в counter.

Таким образом, мы можем получить некорректный результат, а volatile соответственно не смог защитить от "состояния гонки" (race condition) во время составной операции.

Когда использовать volatile

volatile можно назвать "облегченным" механизмом синхронизации, который применяется, когда надо обеспечить видимость изменений переменных между потоками. Однако его следует использовать только тогда, когда выполняются оба следующих условия:

  • Изменения переменной не зависят от ее текущего значения (т.е. вы не делаете counter++ или x = x + 1).

  • Запись в переменную производит только один поток, а ее значение считывают один или несколько других потоков.

Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850
Morty Proxy This is a proxified and sanitized view of the page, visit original site.