Для чего полезно ключевое слово volatile?

Сегодня на работе я наткнулся на ключевое слово volatile в Java. Не будучи очень с ним знаком, я нашел это объяснение.

Учитывая подробности, в которых эта статья объясняет ключевое слово, о котором идет речь, вы когда-нибудь использовали его или могли бы вы когда-нибудь увидеть случай, в котором вы могли бы использовать это ключевое слово правильным образом?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
714
0
273 451
24
Перейти к ответу Данный вопрос помечен как решенный

Ответы 24

«… Модификатор volatile гарантирует, что любой поток, который читает поле, увидит последнее записанное значение».- Джош Блох

Если вы думаете об использовании volatile, прочтите пакет java.util.concurrent, который имеет дело с атомарным поведением. Сообщение в Википедии о Шаблон Singleton показывает использование нестабильности.

Почему есть ключевые слова volatile и synchronized?

ptkato 09.05.2015 05:32

Статья в Википедии о синглтон-шаблоне с тех пор сильно изменилась и больше не содержит упомянутого примера volatile. Его можно найти в архивной версии.

bskp 23.09.2016 15:26

@ptkato Эти два ключевых слова служат совершенно разным целям, поэтому вопрос не имеет особого смысла для сравнения, хотя оба они связаны с параллелизмом. Это все равно, что сказать: «Почему есть ключевые слова void и public?».

DavidS 03.12.2019 22:08

Абсолютно да. (И не только в Java, но и в C#.) Бывают случаи, когда вам нужно получить или установить значение, которое гарантированно будет атомарной операцией на вашей платформе, например, int или boolean, но не требует накладные расходы на блокировку резьбы. Ключевое слово volatile позволяет гарантировать, что при чтении значения вы получите значение Текущий, а не кешированное значение, которое только что стало устаревшим в результате записи в другом потоке.

Ответ принят как подходящий

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

Чтобы ответить на ваш вопрос: Да, я использую переменную volatile, чтобы контролировать, продолжает ли какой-либо код цикл. Цикл проверяет значение volatile и продолжается, если оно равно true. Условие может быть установлено на false, вызвав метод «стоп». Цикл видит false и завершается, когда он проверяет значение после завершения выполнения метода stop.

Книга "Параллелизм Java на практике", которую я настоятельно рекомендую, дает хорошее объяснение volatile. Эта книга написана тем же человеком, который написал статью IBM, на которую ссылается вопрос (на самом деле, он цитирует свою книгу внизу этой статьи). Мое использование volatile - это то, что его статья называет «флагом состояния шаблона 1».

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

Это правильный, но неполный ответ. Он опускает важное свойство volatile, которое пришло с новой моделью памяти Java, определенной в JSR 133: когда поток читает переменную volatile, он видит не только последнее значение, записанное в него каким-то другим потоком, но и все остальные записи в другой поток. переменные, которые были видны в этом другом потоке во время записи volatile. См. этот ответ и эта ссылка.

Adam Zalcman 18.07.2013 16:32

Для новичков я бы попросил вас продемонстрировать с помощью некоторого кода (пожалуйста?)

Hungry Blue Dev 22.02.2014 10:43

В статье, связанной с вопросом, есть примеры кода.

Greg Mattes 23.02.2014 03:37

Я думаю, что связь «Хеннесси и Паттерсон» не работает. А ссылка на «модель памяти Java» на самом деле ведет к Oracle Language Specification «Глава 17. Потоки и блокировки».

Kris 18.09.2015 20:56

@GregMattes, из docs.oracle.com/javase/tutorial/essential/concurrency/…, похоже, что volatile не нужен, если значение является ссылочным полем и имеет значение "bool". Таким образом, "volatile" может даже не требоваться в приведенном вами примере.

jordan 09.10.2015 18:43

@jordan Из вашей ссылки: «Однако [атомарное чтение / запись bools] не устраняет всю необходимость синхронизации атомарных действий, потому что ошибки согласованности памяти все еще возможны. Использование изменчивых переменных снижает риск ошибок согласованности памяти, потому что любая запись в изменчивая переменная устанавливает связь "происходит до" с последующими чтениями той же самой переменной ".

bernie 07.09.2016 17:47

JLS на самом деле не гарантирует, что запись в изменчивую переменную немедленно станет видимой для других потоков. Он утверждает, что the Java Memory Model ensures that all threads see a consistent value for the variable и ссылается на раздел JMM, но этот раздел только обещает, что a write to a volatile variable v synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).. Это гарантирует только то, что @AdamZalcman упомянул выше.

fefrei 22.06.2017 15:52

Я читал сейчас в «Шаблоны проектирования сначала голова», он обычно используется в шаблоне проектирования Singleton, где уникальный объект определяется как изменчивый, поэтому несколько потоков могут видеть, когда изменяется значение. Он также используется с шаблоном двойной проверки.

monolith 26.06.2017 22:27

@wedran: это само по себе противоречие. Если значение может измениться, оно не может быть одноэлементным, поскольку существует как минимум два значения: старое и новое.

Holger 05.07.2017 10:49

@fefrei: «немедленно» - разговорный термин. Конечно, это не может быть гарантировано, если на самом деле не указаны ни время выполнения, ни алгоритмы планирования потоков. Единственный способ для программы узнать, следует ли чтение энергозависимой памяти после конкретной записи энергозависимости, - это проверить, является ли видимое значение ожидаемым записанным.

Holger 05.07.2017 11:06

Одним из распространенных примеров использования volatile является использование переменной volatile boolean в качестве флага для завершения потока. Если вы запустили поток и хотите иметь возможность безопасно прерывать его из другого потока, вы можете настроить поток периодически проверять флаг. Чтобы остановить это, установите флаг в значение true. Установив флаг volatile, вы можете гарантировать, что поток, который его проверяет, увидит, что он установлен, в следующий раз, когда он его проверяет, без необходимости даже использовать блок synchronized.

Да, volatile необходимо использовать всякий раз, когда вы хотите, чтобы к изменяемой переменной могли обращаться несколько потоков. Это не очень распространенный вариант использования, потому что обычно вам нужно выполнить более одной атомарной операции (например, проверить состояние переменной перед ее изменением), и в этом случае вместо этого вы должны использовать синхронизированный блок.

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

Если вы разрабатываете приложение, которое будет развернуто на сервере приложений (Tomcat, JBoss AS, Glassfish и т. д.), Вам не нужно самостоятельно обрабатывать управление параллелизмом, поскольку оно уже адресовано сервером приложений. На самом деле, если я правильно вспомнил, стандарт Java EE запрещает любой контроль параллелизма в сервлетах и ​​EJB, поскольку он является частью уровня «инфраструктуры», от которого вы, как предполагалось, избавились. В таком приложении управление параллелизмом осуществляется только в том случае, если вы реализуете одноэлементные объекты. Это даже уже решено, если вы объединяете свои компоненты с помощью frameworkd, например Spring.

Таким образом, в большинстве случаев разработки Java, когда приложение представляет собой веб-приложение и использует инфраструктуру IoC, такую ​​как Spring или EJB, вам не нужно использовать «volatile».

Да, я использую его довольно часто - он может быть очень полезен для многопоточного кода. Статья, на которую вы указали, хорошая. Однако следует помнить о двух важных вещах:

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

volatile очень полезен для остановки потоков.

Не то чтобы вам следовало писать свои собственные потоки, в Java 1.6 есть много хороших пулов потоков. Но если вы уверены, что вам нужен поток, вам нужно знать, как его остановить.

Шаблон, который я использую для потоков, таков:

public class Foo extends Thread {

  private volatile boolean close = false;

  public void run() {
    while(!close) {
      // do work
    }
  }
  public void close() {
    close = true;
    // interrupt here if needed
  }
}

В приведенном выше сегменте кода поток, считывающий close в цикле while, отличается от потока, вызывающего close(). Без volatile поток, выполняющий цикл, может никогда не увидеть изменения для закрытия.

Обратите внимание, что нет необходимости в синхронизации

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

Jori 06.06.2013 11:27

@Jori, вам нужен volatile, потому что поток, читающий close в цикле while, отличается от того, который вызывает close (). Без volatile поток, выполняющий цикл, может никогда не увидеть изменения для закрытия.

Pyrolistical 06.06.2013 22:48

Вы бы сказали, что есть преимущество между остановкой такого потока или использованием методов Thread # interrupt () и Thread # isInterrupted ()?

Ricardo Belchior 03.09.2015 17:33

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

mr5 11.11.2015 06:44

@Pyrolistical - Вы заметили, что поток никогда видит изменение на практике? Или вы можете расширить пример, чтобы надежно вызвать эту проблему? Мне любопытно, потому что я знаю, что использовал (и видел, как другие используют) код, который в основном идентичен приведенному в примере, но без ключевого слова volatile, и он всегда работает нормально.

aroth 21.02.2016 16:28

@aroth: с сегодняшними JVM вы можете заметить, что на практике, даже с простейшими примерами, однако вы не можете надежно воспроизвести это поведение. С более сложными приложениями у вас иногда есть другие действия с гарантиями видимости памяти внутри вашего кода, которые заставляют его работать, что особенно опасно, поскольку вы не знаете, почему это работает, и простое, очевидно не связанное изменение в вашем коде может сломать ваш заявление…

Holger 18.10.2016 13:39

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

Adrian Liu 08.06.2017 06:55

volatile гарантирует только то, что все потоки, даже они сами, увеличиваются. Например: счетчик одновременно видит одно и то же лицо переменной. Он не используется вместо synchronized, atomic или другого материала, он полностью синхронизирует чтение. Пожалуйста, не сравнивайте его с другими ключевыми словами java. Как показано в примере ниже, операции с изменяемыми переменными также являются атомарными, они сразу завершаются ошибкой или завершаются успешно.

package io.netty.example.telnet;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static volatile  int a = 0;
    public static void main(String args[]) throws InterruptedException{

        List<Thread> list = new  ArrayList<Thread>();
        for(int i = 0 ; i<11 ;i++){
            list.add(new Pojo());
        }

        for (Thread thread : list) {
            thread.start();
        }

        Thread.sleep(20000);
        System.out.println(a);
    }
}
class Pojo extends Thread{
    int a = 10001;
    public void run() {
        while(a-->0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.a++;
            System.out.println("a = "+Main.a);
        }
    }
}

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

    package io.netty.example.telnet;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;

    public class Main {

        public static volatile  AtomicInteger a = new AtomicInteger(0);
        public static void main(String args[]) throws InterruptedException{

            List<Thread> list = new  ArrayList<Thread>();
            for(int i = 0 ; i<11 ;i++){
                list.add(new Pojo());
            }

            for (Thread thread : list) {
                thread.start();
            }

            Thread.sleep(20000);
            System.out.println(a.get());

        }
    }
    class Pojo extends Thread{
        int a = 10001;
        public void run() {
            while(a-->0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Main.a.incrementAndGet();
                System.out.println("a = "+Main.a);
            }
        }
    }

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

  1. Механизм блокировки с двойной проверкой. Часто используется в дизайне Singleton шаблон. В этом объект singleton необходимо объявить изменчивым.
  2. Ложные пробуждения. Поток может иногда просыпаться от ожидающего вызова, даже если уведомляющий вызов не был отправлен. Такое поведение называется ложным пробуждением. Этому можно противостоять, используя условную переменную (логический флаг). Поместите вызов wait () в цикл while, пока установлен флаг. Таким образом, если поток просыпается от вызова ожидания по какой-либо причине, кроме Notify / NotifyAll, тогда он обнаруживает, что флаг все еще верен, и, следовательно, вызовы снова ждут. Перед вызовом notify установите для этого флага значение true. В этом случае логический флаг объявлен как изменчивый.

Весь раздел №2 кажется очень запутанным, он объединяет потерянные уведомления, ложные пробуждения и проблемы с видимостью памяти. Также, если все использования флага синхронизированы, volatile является избыточным. Думаю, я понял вашу точку зрения, но ложное пробуждение - неправильный термин. Просьба уточнить.

Nathan Hughes 25.10.2019 21:12

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

Только переменная-член может быть изменчивой или временной.

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

Чтобы сделать его еще более ясным, НЕ ТРЕБУЕТСЯ устанавливать логическое значение volatile, потому что чтение и запись логического значения УЖЕ атомарны.

Kai Wang 03.04.2017 17:10

@KaiWang вам не нужно использовать volatile для логических значений для целей атомарности. Но вы, конечно, можете по причинам наглядности. Это то, что вы хотели сказать?

SusanW 16.06.2017 15:27

Важный момент о volatile:

  1. Синхронизация в Java возможна с помощью ключевых слов Java synchronized и volatile и блокировок.
  2. В Java у нас не может быть переменной synchronized. Использование ключевого слова synchronized с переменной недопустимо и приведет к ошибке компиляции. Вместо использования переменной synchronized в Java вы можете использовать переменную java volatile, которая инструктирует потоки JVM считывать значение переменной volatile из основной памяти и не кэшировать его локально.
  3. Если переменная не используется совместно несколькими потоками, тогда нет необходимости использовать ключевое слово volatile.

источник

Пример использования volatile:

public class Singleton {
    private static volatile Singleton _instance; // volatile variable
    public static Singleton getInstance() {
        if (_instance == null) {
            synchronized (Singleton.class) {
                if (_instance == null)
                    _instance = new Singleton();
            }
        }
        return _instance;
    }
}

Мы лениво создаем экземпляр, когда приходит первый запрос.

Если мы не сделаем переменную _instancevolatile, то поток, который создает экземпляр Singleton, не сможет взаимодействовать с другим потоком. Таким образом, если поток A создает экземпляр Singleton и сразу после создания ЦП повреждается и т. д., Все другие потоки не смогут увидеть значение _instance как ненулевое, и они будут полагать, что ему все еще присвоено значение null.

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

Conclusion: volatile keyword is also used to communicate the content of memory between threads.

Пример использования без летучих:

public class Singleton{    
    private static Singleton _instance;   //without volatile variable
    public static Singleton getInstance(){   
          if (_instance == null){  
              synchronized(Singleton.class){  
               if (_instance == null) _instance = new Singleton(); 
      } 
     }   
    return _instance;  
    }

Приведенный выше код не является потокобезопасным. Хотя он еще раз проверяет значение экземпляра в синхронизированном блоке (по соображениям производительности), JIT-компилятор может переупорядочить байт-код таким образом, чтобы ссылка на экземпляр была установлена ​​до того, как конструктор завершит свое выполнение. Это означает, что метод getInstance () возвращает объект, который, возможно, не был полностью инициализирован. Чтобы сделать код потокобезопасным, можно использовать ключевое слово volatile начиная с Java 5 для переменной экземпляра. Переменные, помеченные как изменчивые, становятся видимыми для других потоков только после того, как конструктор объекта полностью завершит свое выполнение. Источник

Использование volatile в Java:

Итераторы с быстрым отказом - это как правило, реализованные с использованием счетчика volatile в объекте списка.

  • Когда список обновляется, счетчик увеличивается.
  • Когда создается Iterator, текущее значение счетчика внедряется в объект Iterator.
  • Когда выполняется операция Iterator, метод сравнивает два значения счетчика и выдает ConcurrentModificationException, если они различны.

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

«Итераторы с быстрым отказом обычно реализуются с использованием энергозависимого счетчика» - уже не так, слишком дорого: bugs.java.com/bugdatabase/view_bug.do?bug_id=6625725

Vsevolod Golovanov 24.05.2017 20:15

безопасна ли двойная проверка для _instance? я думал они небезопасны даже с летучими

Dexters 26.11.2017 00:18

"который проинструктирует потоки JVM читать значение изменчивой переменной из основной памяти и не кэшировать его локально". хорошая точка зрения

Humoyun Ahmad 20.12.2017 05:30

Для обеспечения безопасности потоков можно также использовать private static final Singleton _instance;.

Chris311 26.02.2018 12:23

@ Chris311 , конечно, статическое финальное поле, оно потокобезопасное.

Li-Tian 17.10.2020 17:40

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

Если изменения выполняются одним потоком, а остальным нужно просто прочитать это значение, подойдет volatile.

Из документации Oracle страница возникает необходимость в изменчивой переменной для исправления проблем с согласованностью памяти:

Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable.

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

Как объясняется в ответе Peter Parker, в отсутствие модификатора volatile стек каждого потока может иметь свою собственную копию переменной. Сделав переменную volatile, проблемы с согласованностью памяти были исправлены.

Взгляните на страницу руководства Jenkov для лучшего понимания.

Взгляните на соответствующий вопрос SE, чтобы получить более подробную информацию о volatile и сценариях использования volatile:

Разница между volatile и synchronized в Java

Один практический пример использования:

У вас есть много потоков, которым необходимо распечатать текущее время в определенном формате, например: java.text.SimpleDateFormat("HH-mm-ss"). У Yon может быть один класс, который преобразует текущее время в SimpleDateFormat и обновляет переменную каждую секунду. Все другие потоки могут просто использовать эту изменчивую переменную для печати текущего времени в файлах журнала.

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

Есть два разных использования ключевого слова volatile.

  1. Запрещает JVM читать значения из регистра (предполагается, что это кеш) и заставляет его значение считываться из памяти.
  2. Снижает риск ошибок несогласованности памяти.

Prevents JVM from reading values in register, and forces its value to be read from memory.

флаг занятости используется для предотвращения продолжения потока, пока устройство занято и флаг не защищен блокировкой:

while (busy) {
    /* do something else */
}

Поток тестирования продолжится, когда другой поток отключит флаг занятости:

busy = 0;

Однако, поскольку в потоке тестирования к занятости часто обращаются, JVM может оптимизировать тест, поместив значение занятости в регистр, а затем протестировать содержимое регистра, не считывая значение занятости в памяти перед каждым тестом. Тестирующий поток никогда не увидит изменения занятости, а другой поток изменит только значение занятости в памяти, что приведет к тупиковой ситуации. Объявление флаг занятости как изменчивого заставляет его значение считываться перед каждым тестом.

Reduces the risk of memory consistency errors.

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

Техника чтения, записи без ошибок согласованности памяти называется атомное действие.

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

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

  • Чтение и запись являются атомарными для ссылочных переменных и для большинства примитивные переменные (все типы, кроме long и double).
  • Чтение и запись атомарны для всех переменных, объявленных летучий (включая переменные типа long и double).

Ваше здоровье!

Переменная, объявленная с ключевым словом volatile, имеет два основных качества, которые делают ее особенной.

  1. Если у нас есть изменчивая переменная, она не может быть кэширована в кэш-память компьютера (микропроцессора) каким-либо потоком. Доступ всегда происходил из основной памяти.

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

Из двух приведенных выше качеств следует, что

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

А с другой стороны,

  • Если мы дополнительно исследуем # 2, о котором я упоминал, мы увидим, что ключевое слово volatile - идеальный способ поддерживать общую переменную, которая имеет 'n' количество потоков чтения и только один поток писателя для доступа к ней. Как только мы добавим ключевое слово volatile, все будет готово. Никаких других накладных расходов на безопасность потоков.

Наоборот,

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

Это объясняет разницу между энергозависимым и синхронизированным.

ajay 05.09.2019 09:11

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

sergey_o 21.10.2020 20:20

Без «volatile» такая инструкция, как «for (...) {a + = b + c;}» может вообще не ссылаться на ячейки памяти, просто сохраняя «a», «b» и «c» в регистрах для всю продолжительность цикла. Когда значение записывается ЦП в адрес виртуальной памяти (или, если на то пошло, в соответствующий адрес физической памяти), обновление не становится мгновенно видимым для других ЦП и не сбрасывается сразу в ОЗУ [*].

sergey_o 21.10.2020 20:20

Обновление просто помещается в кэш локального ЦП, а затем ставится в очередь на межпроцессорное соединение, которое реализует протокол согласования памяти (например, MESI), и сообщение протокола начинает перемещаться к другим ЦП, в конечном итоге вызывая обновление их кешей. тоже. Это занимает короткое, но ненулевое время. Тем временем другие процессоры не знают, что произошло обновление. Если CPU1 обновил изменчивую переменную X, а CPU2 прочитал ее мгновенно позже, CPU2 может найти либо старое значение для X, либо новое значение для X.

sergey_o 21.10.2020 20:20

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

sergey_o 21.10.2020 20:20

Единственный способ гарантировать считывание «абсолютного последнего» значения переменной - это заблокированная операция, то есть либо блокировка («synchronized», ReenterantLock, whaterver), либо использование классов с блокировкой, таких как AtomicInterger.

sergey_o 21.10.2020 20:21

[*] Загрузка в ОЗУ в любом случае не имеет большого значения, поскольку процессоры не видят ОЗУ напрямую, а только через свои локальные системы кеширования. И когда в системе кеширования отсутствует какое-либо местоположение, нет необходимости заполнять его из ОЗУ: он может знать, что местоположение хранится в другом кэше ЦП (принадлежащем этому ЦП), и, соответственно, будет читать данные не из ОЗУ, а запрашивать это из кэша этого другого ЦП через протокол когерентности кэша межсоединения.

sergey_o 21.10.2020 20:21

Volatile делает следующее.

1> Чтение и запись изменчивых переменных разными потоками всегда происходит из памяти, а не из собственного кеша потока или регистра процессора. Таким образом, каждый поток всегда имеет дело с последним значением. 2> Когда 2 разных потока работают с одним и тем же экземпляром или статическими переменными в куче, один может увидеть действия других как неупорядоченные. См. Блог Джереми Мэнсона по этому поводу. Но здесь помогает volatile.

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

thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3
thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3
thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3

Для этого мы можем использовать следующий полноценный работающий код.

public class Solution {
    static volatile int counter = 0;
    static int print = 0;
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Thread[] ths = new Thread[4];
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new Thread(new MyRunnable(i, ths.length));
            ths[i].start();
        }
    }
    static class MyRunnable implements Runnable {
        final int thID;
        final int total;
        public MyRunnable(int id, int total) {
            thID = id;
            this.total = total;
        }
        @Override
        public void run() {
            // TODO Auto-generated method stub
            while (true) {
                if (thID == counter) {
                    System.out.println("thread " + thID + " prints " + print);
                    print++;
                    if (print == total)
                        print = 0;
                    counter++;
                    if (counter == total)
                        counter = 0;
                } else {
                    try {
                        Thread.sleep(30);
                    } catch (InterruptedException e) {
                        // log it
                    }
                }
            }
        }
    }
}

В следующей ссылке на github есть файл readme, который дает правильное объяснение. https://github.com/sankar4git/volatile_thread_ordering

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

// Code to prove importance of 'volatile' when state of one thread is being mutated from another thread.
// Try running this class with and without 'volatile' for 'state' property of Task class.
public class VolatileTest {
    public static void main(String[] a) throws Exception {
        Task task = new Task();
        new Thread(task).start();

        Thread.sleep(500);
        long stoppedOn = System.nanoTime();

        task.stop(); // -----> do this to stop the thread

        System.out.println("Stopping on: " + stoppedOn);
    }
}

class Task implements Runnable {
    // Try running with and without 'volatile' here
    private volatile boolean state = true;
    private int i = 0;

    public void stop() {
        state = false;
    } 

    @Override
    public void run() {
        while(state) {
            i++;
        }
        System.out.println(i + "> Stopped on: " + System.nanoTime());
    }
}

Когда volatile не используется: вы никогда не увидите сообщение «Остановлен на: xxx» даже после «Остановка на: xxx», и программа продолжит работу.

Stopping on: 1895303906650500

Когда volatile используется:, вы сразу увидите "Остановлен на: xxx".

Stopping on: 1895285647980000
324565439> Stopped on: 1895285648087300

Демо: https://repl.it/repls/SilverAgonizingObjectcode

Голосующему против: Объясните, почему голосовал против? Если это неправда, по крайней мере, я узнаю, что не так. Я добавил этот же комментарий дважды, но не знаю, кто удаляет снова и снова

manikanta 15.09.2019 13:55

То же самое объясняется и здесь: Как можно надежно выйти из потока с помощью внешней переменной условия?

manikanta 26.11.2019 07:33

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

volatile => synchronized[About]

volatile говорит программисту, что значение всегда будет актуальным. Проблема в том, что значение можно сохранить на разных типах аппаратной памяти. Например, это могут быть регистры ЦП, кеш ЦП, ОЗУ ... Регистры СPU и кеш ЦП принадлежат ЦП и не могут обмениваться данными, в отличие от ОЗУ, которое находится на помощь в многопоточной среде.

Ключевое слово volatile говорит, что переменная будет читать и писать из / в RAM-память напрямую. Он имеет некоторую вычислительную нагрузку

Java 5 расширил volatile, поддерживая happens-before[About]

A write to a volatile field happens-before every subsequent read of that field.

Ключевое слово volatileне лечит - ситуация race condition, когда несколько потоков могут одновременно использовать записывать для некоторых значений. Ответ - ключевое слово synchronized[About]

В результате это безопасно только тогда, когда поток одинпишет и другие просто читают значение volatile

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

1.

ЗАПУСК КОДА БЕЗ ИСПОЛЬЗОВАНИЯ ЛЕТУЧИХ СРЕДСТВ

public class VisibilityDemonstration {

private static int sCount = 0;

public static void main(String[] args) {
    new Consumer().start();
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        return;
    }
    new Producer().start();
}

static class Consumer extends Thread {
    @Override
    public void run() {
        int localValue = -1;
        while (true) {
            if (localValue != sCount) {
                System.out.println("Consumer: detected count change " + sCount);
                localValue = sCount;
            }
            if (sCount >= 5) {
                break;
            }
        }
        System.out.println("Consumer: terminating");
    }
}

static class Producer extends Thread {
    @Override
    public void run() {
        while (sCount < 5) {
            int localValue = sCount;
            localValue++;
            System.out.println("Producer: incrementing count to " + localValue);
            sCount = localValue;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                return;
            }
        }
        System.out.println("Producer: terminating");
    }
}
}

В приведенном выше коде есть два потока - производитель и потребитель.

Производящий поток повторяет цикл 5 раз (с задержкой в ​​1000 миллисекунд или 1 секунду) между ними. На каждой итерации поток-производитель увеличивает значение переменной sCount на 1. Таким образом, производитель изменяет значение sCount с 0 на 5 на всех итерациях.

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

Обе петли запускаются одновременно. Таким образом, и производитель, и потребитель должны напечатать значение sCount 5 раз.

ВЫХОД

Consumer: detected count change 0
Producer: incrementing count to 1
Producer: incrementing count to 2
Producer: incrementing count to 3
Producer: incrementing count to 4
Producer: incrementing count to 5
Producer: terminating

АНАЛИЗ

В приведенной выше программе, когда поток-производитель обновляет значение sCount, он действительно обновляет значение переменной в основной памяти (памяти, из которой каждый поток будет первоначально читать значение переменной). Но потребительский поток считывает значение sCount только в первый раз из этой основной памяти, а затем кэширует значение этой переменной в своей собственной памяти. Таким образом, даже если значение исходного sCount в основной памяти было обновлено потоком-производителем, поток-потребитель читает из своего кэшированного значения, которое не обновляется. Это называется ПРОБЛЕМА ВИДИМОСТИ.

2.

ЗАПУСК КОДА ПРИ ИСПОЛЬЗОВАНИИ ЛЕТУЧИХ

В приведенном выше коде замените строку кода, в которой объявлено sCount, следующим образом:

private volatile  static int sCount = 0;

ВЫХОД

Consumer: detected count change 0
Producer: incrementing count to 1
Consumer: detected count change 1
Producer: incrementing count to 2
Consumer: detected count change 2
Producer: incrementing count to 3
Consumer: detected count change 3
Producer: incrementing count to 4
Consumer: detected count change 4
Producer: incrementing count to 5
Consumer: detected count change 5
Consumer: terminating
Producer: terminating

АНАЛИЗ

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

Поскольку значение переменной sCount никогда не кэшируется никаким потоком, потребитель всегда считывает исходное значение sCount из основной памяти (где оно обновляется потоком-производителем). Итак, в этом случае вывод правильный, когда оба потока выводят разные значения sCount 5 раз.

Таким образом, ключевое слово volatile решает проблему ПРОБЛЕМА ВИДИМОСТИ.

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

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

public class SharedObject {
    public volatile int sharedVariable = 0;
}

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


Пример:

Представьте себе ситуацию, в которой два или более потока имеют доступ к общему объекту, который содержит переменную счетчика, объявленную следующим образом:

public class SharedObject {
    public int counter = 0;
}

Представьте также, что только поток 1 увеличивает переменную счетчика, но поток 1 и поток 2 могут время от времени читать переменную счетчика.

Если переменная счетчика не объявлена ​​энергозависимой, нет гарантии, когда значение переменной счетчика будет записано из кеша ЦП обратно в основную память. Это означает, что значение переменной счетчика в кэше ЦП может отличаться от значения в основной памяти. Эта ситуация проиллюстрирована здесь:

volatile

Проблема с потоками, которые не видят последнее значение переменной, потому что оно еще не было записано обратно в основную память другим потоком, называется проблемой «видимости». Обновления одного потока не видны другим потокам.

Другие вопросы по теме