Обеспечивает ли ключевое слово volatile в Java атомарность операций?

Ответ

Нет, volatile не обеспечивает атомарность операций.

Что гарантирует volatile:

  1. Видимость изменений: Гарантирует, что все потоки увидят самое актуальное значение переменной. Запись в volatile-переменную происходит в основную память, минуя кэши процессоров.
  2. Запрет переупорядочивания: Компилятор и процессор не могут переупорядочить операции с volatile-переменной относительно других операций чтения/записи.

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

Пример проблемы:

volatile int counter = 0;
// Операция НЕ атомарна
counter++; // Эквивалентно: int temp = counter; temp = temp + 1; counter = temp;

Атомарные альтернативы:

  • Классы из java.util.concurrent.atomic:
    AtomicInteger atomicCounter = new AtomicInteger(0);
    atomicCounter.incrementAndGet(); // Атомарная операция
  • Синхронизация:

    private final Object lock = new Object();
    private int counter = 0;
    
    public void increment() {
        synchronized(lock) {
            counter++;
        }
    }

Ответ 18+ 🔞

А, слушай, смотри, тут народ часто путается, как пьяный в луже. volatile — это не волшебная палочка, которая делает всё атомарным, нет, блядь.

Что он реально делает, этот volatile?

  1. Видимость, ёпта! Гарантирует, что если один поток впендюрил новое значение в переменную, то все остальные потоки это увидят сразу, а не будут до скончания века читать свою закэшированную копию из какого-нибудь регистра. Прямая запись в основную память, минуя все эти буферы.
  2. Порядок, сука! Компилятору и процессору запрещено переставлять операции с этой переменной туда-сюда как попало относительно других операций чтения/записи. Это чтобы не было неожиданных сюрпризов.

А в чём подвох-то? Да в том, что сложная операция — это не один шаг! Возьмём этот проклятый инкремент i++. Он же из трёх действий состоит: прочитать значение, прибавить к нему единицу, записать обратно. И вот представь: поток А прочитал, скажем, 5. Только собрался записать 6, как его вытеснили. Поток Б успевает прочитать всё те же 5, прибавить и записать 6. Потом поток А возвращается и тупо перезаписывает тем же 6. И где наше обновление? Потерялось, в рот меня чих-пых! volatile тут нихуя не поможет, потому что прерваться могут между этими шагами.

Вот, смотри, наглядный пиздец:

volatile int counter = 0;
// Это НЕ атомарно, запомни раз и навсегда!
counter++; // По факту: int temp = counter; temp = temp + 1; counter = temp;

Так что же делать, если надо по-честному?

  • Вот эти ребята из java.util.concurrent.atomic — красавчики: Они делают это на уровне процессора.
    AtomicInteger atomicCounter = new AtomicInteger(0);
    atomicCounter.incrementAndGet(); // Вот это уже атомарная операция, без всяких там.
  • Старая добрая синхронизация: Ну, если очень хочется.

    private final Object lock = new Object();
    private int counter = 0;
    
    public void increment() {
        synchronized(lock) { // Заперлись и делаем спокойно
            counter++;
        }
    }

Короче, volatile — это про видимость и порядок, а не про атомарность. Не путай, а то будет тебе волнение ебать и терпения ноль ебать при отладке.