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

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

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

Однако я до сих пор не могу понять, в каком сценарии мы должны его использовать. Я имею в виду, может ли кто-нибудь предоставить какой-либо пример кода, который может доказать, что использование «volatile» приносит пользу или решает проблемы по сравнению с его использованием?


person Eric Jiang    schedule 28.04.2011    source источник
comment
volatile (и многие другие аспекты, связанные с параллелизмом) очень трудно продемонстрировать, потому что при неправильном использовании они может по-прежнему работать правильно и не показывать проблему. пока не возникнет очень специфическое условие, которое приведет к проблеме.   -  person Joachim Sauer    schedule 28.04.2011
comment
Я пробовал это сам и создал тест, который успешно передает значения между потоками с использованием невольтильного поля один миллиард раз, но относительно быстро терпит неудачу, когда загружается одно и то же поле. Даже рассчитать влияние volatile на производительность очень сложно, так как это зависит от множества факторов.   -  person Peter Lawrey    schedule 28.04.2011
comment
Взгляните на этот пост: stackoverflow.com/questions/7855700/   -  person Ravindra babu    schedule 10.05.2018


Ответы (6)


Вот пример того, почему volatile необходимо. Если вы удалите ключевое слово volatile, поток 1 может никогда не завершиться. (Когда я тестировал Java 1.6 Hotspot в Linux, это действительно было так — ваши результаты могут отличаться, поскольку JVM не обязана кэшировать переменные, не отмеченные volatile.)

public class ThreadTest {
  volatile boolean running = true;

  public void test() {
    new Thread(new Runnable() {
      public void run() {
        int counter = 0;
        while (running) {
          counter++;
        }
        System.out.println("Thread 1 finished. Counted up to " + counter);
      }
    }).start();
    new Thread(new Runnable() {
      public void run() {
        // Sleep for a bit so that thread 1 has a chance to start
        try {
          Thread.sleep(100);
        } catch (InterruptedException ignored) { 
         // catch block
        }
        System.out.println("Thread 2 finishing");
        running = false;
      }
    }).start();
  }

  public static void main(String[] args) {
    new ThreadTest().test();
  }
}
person Simon Nickerson    schedule 28.04.2011
comment
Извините, я много раз пытался удалить ключевое слово volatile и запускать программу, но каждый раз поток 1 может быть завершен. Я думаю, что для кода, который вы предоставили, даже без volatile thread 1 всегда можно завершить каждый раз. - person Eric Jiang; 28.04.2011
comment
@Eric: это дает эффект, который я описываю, на двух разных машинах с Java 6 JVM. Я думаю, я должен был сформулировать это по-другому. Завершение гарантировано только в том случае, если присутствует ключевое слово volatile. Если его нет, JVM может кэшировать значение running, поэтому поток 1 не должен завершаться. Однако это не обязательно кешировать значение. - person Simon Nickerson; 28.04.2011
comment
Сохранение volatile остановит поток 1, как только для «running» будет установлено значение «false» в потоке 2. Но если удалить volatile, поток 1 может не завершиться даже после того, как «running» будет установлено значение «false» в потоке 2, когда поток завершится, зависит от того, когда JVM обновляет текущее значение в потоке 1. Вы имеете в виду это??? Но если в потоке 2 задано значение «false», почему поток 1 не может получить правильное значение??? Если это так, я думаю, что JVM ведет себя неправильно. - person Eric Jiang; 28.04.2011
comment
JVM не ведет себя неправильно. Без volatile (или синхронизации, или чего-то еще, что вызывает отношение "происходит до"), поток 1 не должен видеть изменения, сделанные потоком 2. - person Simon Nickerson; 28.04.2011
comment
@Eric: именно в этом проблема: правильное поведение не эквивалентно интуитивному поведению. Виртуальные машины JVM могут кэшировать значения (например, в регистре ЦП), если они считываются повторно и не должны каждый раз считывать их из основной памяти. Указание переменной volatile отключает это кэширование! - person Joachim Sauer; 28.04.2011
comment
@Simon Nickerson: спасибо за помощь. Как насчет этого примера: class TestThread extends Thread { private boolean running = true; public void run() { while(running) { //do something } } public void setStop() { running = false; } } Метод setStop будет вызываться в другом потоке, который хочет остановить TestThread. Итак, здесь я не помечал выполнение как volatile, поэтому есть вероятность, что TestThread не остановится немедленно (даже, возможно, никогда не остановится) после вызова setStop() в другом потоке, верно? Здесь безопасный способ заключается в том, что мне нужно пометить работу как изменчивую, верно? - person Eric Jiang; 29.04.2011
comment
Спасибо Саймон! Вы мне очень помогли. - person Eric Jiang; 30.04.2011
comment
эй, Саймон, я проверил ваш код на 1.7 HotSpot, и он работает, как вы упомянули, если я удалю ключевое слово volatile, то Thread1 никогда не завершится, но странно то, что если вы удалите код сна, который вы добавили перед запуском T2, тогда volatile не завершится предоставить какое-либо преимущество. - person Diego Ramos; 08.02.2021

Ниже приведен канонический пример необходимости volatile (в данном случае для переменной str. Без нее точка доступа поднимает доступ за пределы цикла (while (str == null)) и run() никогда не завершается. Это произойдет на большинстве серверных JVM.

public class DelayWrite implements Runnable {
  private String str;
  void setStr(String str) {this.str = str;}

  public void run() {
    while (str == null);
    System.out.println(str);
  }

  public static void main(String[] args) {
    DelayWrite delay = new DelayWrite();
    new Thread(delay).start();
    Thread.sleep(1000);
    delay.setStr("Hello world!!");
  }
}
person Jed Wesley-Smith    schedule 28.04.2011
comment
Уэсли: Большое спасибо!!! Я вижу проблему с java -server для запуска JVM и запуска приложения. Теперь я получил ответ, спасибо! - person Eric Jiang; 29.04.2011
comment
новый поток(весело).start(); эта строка должна быть новой Thread(delay).start(); верный ? - person Dhanaraj Durairaj; 04.02.2015
comment
Отлично и полезно! У меня есть еще вопросы: 1) Почему вы используете «канонический»? 2) Как я могу получить код сборки, когда код Java работает? Могу ли я получить подсказку от сборки, например, другие демонстрации кода cpp? - person gfan; 18.03.2017

Эрик, я читал ваши комментарии, и один из них меня особенно поражает.

На самом деле, я могу понять использование volatile на концептуальном уровне. Но для практики я не могу придумать код с проблемами параллелизма без использования volatile

Очевидная проблема, с которой вы можете столкнуться, - это переупорядочение компилятора, например, более известный подъем, упомянутый Саймоном Никерсоном. Но допустим, что перестановок не будет, что комментарий может быть действительным.

Другая проблема, которую решает volatile, связана с 64-битными переменными (long, double). Если вы записываете в long или double, они рассматриваются как два отдельных 32-битных хранилища. Что может произойти с параллельной записью, так это то, что старшие 32 одного потока записываются в старшие 32 бита регистра, в то время как другой поток записывает младшие 32 бита. Можно потом иметь лонг что ни то ни другое.

Кроме того, если вы посмотрите на раздел памяти JLS, вы обнаружите, что это нестрогая модель памяти.

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

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

person John Vint    schedule 28.04.2011

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

Они выделяют этот пример:

class Test {
    static volatile int i = 0, j = 0;
    static void one() { i++; j++; }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

Это означает, что в течение one() j никогда не превышает i. Однако другой поток, выполняющий two(), может вывести значение j, которое намного больше, чем i, поскольку, скажем, two() выполняется и получает значение i. Затем one() выполняется 1000 раз. Затем поток, выполняющий два, наконец, снова назначается и получает j, который теперь намного больше, чем значение i. Я думаю, что этот пример прекрасно демонстрирует разницу между volatile и синхронизированным — обновления i и j являются volatile, что означает, что порядок, в котором они происходят, соответствует исходному коду. Однако два обновления происходят отдельно, а не атомарно, поэтому вызывающие могут видеть значения, которые выглядят (для этого вызывающего) несовместимыми.

В двух словах: будьте очень осторожны с volatile!

person alpian    schedule 28.04.2011
comment
По предоставленной вами ссылке приведен пример ниже: class Test { static volatile int i = 0, j = 0; статическая пустота one() { i++; j++; } static void two() { System.out.println(i= + i + j= + j); } } Это позволяет одновременно выполнять первый и второй методы, но гарантирует, что доступ к общим значениям для i и j будет происходить ровно столько раз и в том же порядке, что и во время выполнения текста программы. по каждому потоку. Но я не думаю, что пример правильный, использование volatile' or not дает тот же результат, у j всегда есть шанс отличаться от i. - person Eric Jiang; 28.04.2011
comment
@Eric Jiang - Дело в том, что j может быть больше, чем i. Хотя i всегда увеличивается первым. Если оба изменчивы, они все равно могут быть разными, но j не может быть больше i. - person Ishtar; 28.04.2011
comment
Я обновил свой ответ, чтобы объяснить, что, по моему мнению, говорит JLS (хотя я думаю, что JLS сказал это очень хорошо в первую очередь) :) - person alpian; 28.04.2011
comment
Спасибо, alpian, на самом деле, я очень хорошо улавливаю ваш смысл, но когда я запускаю код и тестирую много раз, я никогда не вижу, чтобы J было больше, чем i. Связано ли это со средой моего ПК, например, с версией JDK или аппаратным обеспечением? Могу ли я понять, что даже если код выполняется сотни тысяч раз, может быть один раз, когда J больше, чем i (просто есть риск, но, возможно, этого не произойдет, поэтому для гарантии нужно использовать «изменчивость»), верно? - person Eric Jiang; 29.04.2011

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

public class VolatileExample {

    private static volatile boolean BOOL = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> { while (BOOL) { } }).start();
        TimeUnit.MILLISECONDS.sleep(500);
        BOOL = false;
    }
}
person Arnaud B    schedule 30.10.2017

Чтобы расширить ответ @jed-wesley-smith, если вы добавите это в новый проект, удалите ключевое слово volatile из iterationCount и запустите его, он никогда не остановится. Добавление ключевого слова volatile к str или iterationCount приведет к успешному завершению кода. Я также заметил, что сон не может быть меньше 5 при использовании Java 8, но, возможно, ваш пробег может отличаться от других версий JVM/Java.

public static class DelayWrite implements Runnable
{
    private String str;
    public volatile int iterationCount = 0;

    void setStr(String str)
    {
        this.str = str;
    }

    public void run()
    {
        while (str == null)
        {
            iterationCount++;
        }
        System.out.println(str + " after " + iterationCount + " iterations.");
    }
}

public static void main(String[] args) throws InterruptedException
{
    System.out.println("This should print 'Hello world!' and exit if str or iterationCount is volatile.");
    DelayWrite delay = new DelayWrite();
    new Thread(delay).start();
    Thread.sleep(5);
    System.out.println("Thread sleep gave the thread " + delay.iterationCount + " iterations.");
    delay.setStr("Hello world!!");
}
person Aron    schedule 12.11.2018