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

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

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

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

Ответ 1

Вот пример того, почему 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) { }
        System.out.println("Thread 2 finishing");
        running = false;
      }
    }).start();
  }

  public static void main(String[] args) {
    new ThreadTest().test();
  }
}

Ответ 2

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

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!!");
  }
}

Ответ 3

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

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

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

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

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

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

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

Ответ 4

Ключевое слово 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, потому что пусть say two() работает и выбирает значение i. Затем one() выполняется 1000 раз. Затем поток, выполняющий два, снова получает запланированное расписание и выбирает j, который теперь намного больше значения i. Я думаю, что этот пример отлично демонстрирует разницу между изменчивым и синхронизированным - обновления для i и j являются volatile, что означает, что порядок, в котором они происходят, согласуется с исходным кодом. Однако два обновления происходят отдельно, а не атомарно, поэтому вызывающие могут видеть значения, которые выглядят (для этого вызывающего), чтобы быть непоследовательными.

Вкратце: будьте очень осторожны с volatile!

Ответ 5

Минималистский пример в 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;
    }
}