Рубрики
Без рубрики

Руководство по ключевому слову Volatile в Java

Узнайте о ключевом слове Java volatile и его возможностях.

Автор оригинала: baeldung.

1. Обзор

При отсутствии необходимых синхронизаций компилятор, среда выполнения или процессоры могут применять всевозможные оптимизации. Несмотря на то, что эти оптимизации в большинстве случаев полезны, иногда они могут вызвать тонкие проблемы.

Кэширование и переупорядочение относятся к числу тех оптимизаций, которые могут удивить нас в параллельных контекстах. Java и JVM предоставляют множество способов управления порядком памяти , и ключевое слово volatile является одним из них.

В этой статье мы сосредоточимся на этой основополагающей, но часто неправильно понимаемой концепции в языке Java – ключевом слове volatile . Во-первых, мы начнем с небольшой информации о том, как работает базовая компьютерная архитектура, а затем познакомимся с порядком памяти в Java.

2. Общая многопроцессорная Архитектура

Процессоры отвечают за выполнение инструкций программы. Поэтому им необходимо извлекать как инструкции программы, так и необходимые данные из оперативной памяти.

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

Здесь в игру вступает следующая иерархия памяти:

По мере того как различные ядра выполняют больше инструкций и манипулируют большим количеством данных, они заполняют свои кэши более релевантными данными и инструкциями. Это улучшит общую производительность за счет введения согласованности кэша проблем .

Проще говоря, мы должны дважды подумать о том, что происходит, когда один поток обновляет кэшированное значение.

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

Чтобы подробнее рассказать о согласованности кэша, давайте позаимствуем один пример из книги Параллелизм Java на практике :

public class TaskRunner {

    private static int number;
    private static boolean ready;

    private static class Reader extends Thread {

        @Override
        public void run() {
            while (!ready) {
                Thread.yield();
            }

            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new Reader().start();
        number = 42;
        ready = true;
    }
}

Класс TaskRunner поддерживает две простые переменные. В своем основном методе он создает другой поток, который вращается на переменной ready до тех пор, пока она false. Когда переменная станет true, поток просто напечатает переменную number .

Многие могут ожидать, что эта программа просто напечатает 42 после небольшой задержки. Однако на самом деле задержка может быть гораздо более длительной. Он может даже висеть вечно или даже печатать ноль!

Причиной этих аномалий является отсутствие надлежащей видимости памяти и переупорядочения . Давайте рассмотрим их более подробно.

3.1. Видимость памяти

В этом простом примере у нас есть два потока приложений: основной поток и поток чтения. Давайте представим себе сценарий, в котором ОС планирует эти потоки на двух разных ядрах процессора, где:

  • Основной поток имеет свою копию переменных ready и number в своем основном кэше
  • Поток чтения также заканчивается своими копиями
  • Основной поток обновляет кэшированные значения

На большинстве современных процессоров запросы на запись не будут применяться сразу после их выдачи. На самом деле, процессоры, как правило, помещают эти записи в очередь в специальный буфер записи . Через некоторое время они сразу же применят эти записи в основную память.

Учитывая все сказанное, когда основной поток обновляет переменные number и ready , нет никакой гарантии относительно того, что может увидеть поток чтения. Другими словами, поток чтения может увидеть обновленное значение сразу, или с некоторой задержкой, или вообще никогда!

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

3.2. Изменение порядка

Что еще хуже, поток чтения может видеть эти записи в любом порядке, отличном от фактического порядка программы . Например, с момента первого обновления переменной number :

public static void main(String[] args) { 
    new Reader().start();
    number = 42; 
    ready = true; 
}

Мы можем ожидать, что читательская нить печатает 42. Однако на самом деле можно увидеть ноль в качестве печатного значения!

Переупорядочение – это метод оптимизации для повышения производительности. Интересно, что различные компоненты могут применять эту оптимизацию:

  • Процессор может очистить свой буфер записи в любом порядке, отличном от порядка программы
  • Процессор может применять технику исполнения вне ордера
  • JIT-компилятор может оптимизировать работу с помощью переупорядочения

3.3. порядок энергозависимой памяти

Чтобы гарантировать, что обновления переменных предсказуемо распространяются на другие потоки, мы должны применить летучий модификатор для этих переменных:

public class TaskRunner {

    private volatile static int number;
    private volatile static boolean ready;

    // same as before
}

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

4. летучая и потоковая синхронизация

Для многопоточных приложений нам необходимо обеспечить несколько правил для согласованного поведения:

  • Взаимное исключение – только один поток выполняет критический раздел за раз.
  • Видимость – изменения, внесенные одним потоком в общие данные, видны другим потокам для поддержания согласованности данных.

синхронизированные методы и блоки обеспечивают оба вышеуказанных свойства за счет производительности приложения.

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

5. Бывает-Перед Заказом

Эффекты видимости памяти переменных volatile выходят за рамки самих переменных volatile .

Чтобы сделать вещи более конкретными, предположим, что поток A записывает в переменную volatile , а затем поток B считывает ту же переменную volatile . В таких случаях значения, которые были видны A до записи переменной volatile , будут видны B после чтения переменной volatile :

Технически говоря, любая запись в поле volatile происходит перед каждым последующим чтением того же поля . Это правило volatile variable модели памяти Java ( JMM ).

5.1. Свиноводство

Из-за силы упорядочения памяти до того, как это произойдет, иногда мы можем использовать свойства видимости другой изменчивой переменной . Например, в нашем конкретном примере нам просто нужно пометить переменную ready как volatile :

public class TaskRunner {

    private static int number; // not volatile
    private volatile static boolean ready;

    // same as before
}

Все, что было до записи true в переменную ready , видно любому после чтения переменной ready . Таким образом, переменная number piggybacks в области видимости памяти принудительно выполняется переменной ready . Проще говоря , даже если это не изменчивая переменная, она демонстрирует изменчивое поведение.

Используя эту семантику, мы можем определить только несколько переменных в нашем классе как volatile и оптимизировать гарантию видимости.

6. Заключение

В этом уроке мы подробнее изучили ключевое слово volatile и его возможности, а также улучшения, внесенные в него, начиная с Java 5.

Как всегда, примеры кода можно найти на GitHub .