1. Обзор

В этой быстрой статье мы сосредоточимся на фундаментальной, но часто неправильно понятой концепции в языке Java - ключевом слове volatile.

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

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

2. Когда использовать volatile

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

Это можно проиллюстрировать на простом примере:

public class SharedObject {
    private volatile int count = 0;
   
    public void increamentCount() {      
        count++;
    }
    public int getCount() {
        return count;
    }
}

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

Конечно, можно также избежать состояния гонки с использованием предоставляемых Java типов атомарных данных, таких как AtomicInt или AtomicLong.

3. Volatile и потоковая синхронизация

Для всех многопоточных приложений нам необходимо обеспечить пару правил для согласованного поведения:

  • Взаимное исключение - только один поток выполняет критический раздел за один раз
  • Изменения видимости, сделанные одним потоком для общих данных, видны другим потокам для обеспечения согласованности данных

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

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

4. Happens-Before гарантия

Начиная с Java 5, ключевое слово volatile также предоставляет дополнительные возможности, которые обеспечивают запись значений всех переменных, включая не-volatile, в основную память вместе с операцией записи Volatile.

Это называется Happens-Before, так как оно дает видимость всех переменных другому потоку чтения. Кроме того, JVM не меняет порядок чтения и записи изменчивых переменных.

Давайте посмотрим на пример:

Thread 1
    object.aNonValitileVariable = 1;
    object.aVolatileVariable = 100; // volatile write

Thread 2:
    int aNonValitileVariable = object.aNonValitileVariable;
    int aVolatileVariable =  object.aVolatileVariable;

В этом случае, когда Thread записывает значение aVolatileVariable, значение aNonValitileVariable также записывается в основную память. И хотя это не volatile переменная, она демонстрирует изменчивое поведение.

Используя эту семантику, мы можем определить только несколько переменных в нашем классе как volatile и оптимизировать гарантию видимости.

5. Заключение

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