Как я могу правильно использовать ключевое слово volatile в Java?

Скажем, у меня есть два потока и объект. Один поток назначает объект:

public void assign(MyObject o) {
    myObject = o;
}

Другой поток использует объект:

public void use() {
    myObject.use();
}

Следует ли объявлять переменную myObject как изменчивую? Я пытаюсь понять, когда использовать volatile, а когда нет, и это меня озадачивает. Возможно ли, что второй поток содержит ссылку на старый объект в своем кеше локальной памяти? Если нет, почему бы и нет?

Большое спасибо.

Ответы

Ответ 1

Я пытаюсь понять, когда использовать летучий, а когда не

Вам следует избегать его использования. Вместо этого используйте AtomicReference (или другое atomic класс, где это необходимо). Эффекты памяти одинаковы, а намерение намного яснее.

Я очень рекомендую прочитать превосходную Java Concurrency на практике для лучшего понимания.

Ответ 2

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

public synchronized void doSomething() {}

Если вы хотите "синхронизировать" доступ к переменным, вы хотели бы использовать модификатор volatile:

private volatile SomeObject variable;

За кулисами они выполняют разные вещи, но эффект один и тот же: изменения сразу видны для следующего потока доступа.

В вашем конкретном случае я не думаю, что модификатор volatile имеет любое значение. volatile не гарантирует, что поток, назначающий объект, будет выполняться перед потоком с использованием объекта. Это может быть так же хорошо. Вероятно, вы просто хотите сначала выполнить nullcheck в методе use().

Обновить: также см. в этой статье:

Доступ к переменной действует как бы, заключен в синхронизированный блок, синхронизированный сам по себе. Мы говорим, что "действует как бы" во втором пункте, потому что для программиста, по крайней мере (и, вероятно, в большинстве реализаций JVM), не задействован фактический объект блокировки.

Ответ 3

Объявление изменчивой переменной Java означает:

  • Значение этой переменной никогда не будет кэшировано thread-local
  • Доступ к переменной действует так, как если бы она была заключена в синхронизированный блок

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

public class StoppableThread extends Thread {
  private volatile boolean stop = false;

  public void run() {
    while (!stop) {
      // do work 
    }
  }

  public void stopWork() {
    stop = true;
  }
}

Ответ 4

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

Ответ 5

Я потратил довольно много времени на понимание ключевого слова volatile. Я думаю, что @aleroot предоставил лучший и самый простой пример в мире.

Это в свою очередь мое объяснение для манекенов (например, я:-)):

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

  • stopWork(): Мне нужно установить stop в true
  • Отлично, я сделал это в своем локальном стеке, теперь мне нужно обновить основную кучу JVM.
  • Упс, JVM говорит мне дать путь в CPU к другому потоку, я должен остановиться на некоторое время...
  • Хорошо, я вернулся. Теперь я могу обновить основную кучу с моей стоимостью. Обновление...

Сценарий2: Теперь пусть stop будет объявлен как изменчивый:

  • stopWork(): Мне нужно установить stop в true
  • Отлично, я сделал это в своем локальном стеке, теперь мне нужно обновить основную кучу JVM.
  • Извините, ребята, я должен сделать (2) СЕЙЧАС - мне сказали, что это volatile. Я должен занять процессор немного дольше...
  • Обновление основной кучи...
  • Хорошо, все готово. Теперь я могу уступить.

Нет синхронизации, просто простая идея...

Почему бы не объявить все переменные volatile на всякий случай? Из-за Scenario2/Step3. Это немного неэффективно, но все же лучше обычной синхронизации.

Ответ 6

Здесь есть некоторые путаные комментарии: уточнить, ваш код некорректен, поскольку два разных потока вызывают assign() и use().

В отсутствие volatile или другой происходит до отношения (например, синхронизация по общей блокировке) любая запись в myObject в assign() не гарантируется видимым потоком, вызывающим use() - не сразу, не своевременно, а действительно никогда.

Да, volatile - это один из способов исправить это (если это неправильное поведение - есть вероятные ситуации, в которых вас это не волнует!).

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