Ключевое слово volatile - это еще один способ (как synchronized, atomic wrapper) сделать класс потокобезопасным (Thread safe). Потокобезопасность означает что метод или класс может быть использован множеством потоков в одно и то же время без проблем.

Вот следующие пример:

class SharedObj
{
   // Изменения переменной sharedVar в одном потоке
   // могут не применится в это же вермя в другом потоке
   static int sharedVar = 6;
}

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

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

Java threads

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

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

class SharedObj
{
   // volatile keyword here makes sure that
   // the changes made in one thread are
   // immediately reflect in other thread
   static volatile int sharedVar = 6;
}

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

volatile vs synchronized

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

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

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

В некоторых случаях мы можем захотеть обеспечаить видимость данных без атомарности. Используя ключевое слово synchronized в этих случаях может быть убийственным и может вызывать проблемы с масштабированием. Таким образом на помощь приходит ключевое слово volatile. Переменные volatile обеспечивают видимость и синхронизацию, но не атомарность. Переменные volatile никогда не будут закэшированы и все операции чтения/записи будут выполняться только из основной памяти. Все же, использование volatile имеет ограничение для некоторых редких случаев когда необходимо обеспечить атомарность. Например, простой инкремент переменной, такой как  x = x+1; или x++ выглядит как одна операции, однако, на самом деле это последовательность операций чтения-изменения-записи, которые должны выполняться  атомарно.

volatile в Java vs C/C++:

volatile в Java и в C++ это разные вещи. Для Java, volatile объясняет компилятору что значение переменной никогда не должно кэшироваться, потому что это значение может быть изменено снаружи. В C++ volatile необходим когда разрабатываются драйвера для подключаемых устройств, где необходимо считывать и записать данные в память устройства. Содержимое памяти этих подключаемых устройтсв может измениться в любой момент, поэтому необходимо использовать volatile.

Ссылки:

Эта статья является переводом этой статьи.