Java Memory Model
У каждого ядра процессора свой кэш. Изменение переменной одним потоком может не дойти до другого потока сразу — он видит старое значение из своего кэша, а не из общей памяти.
Java Memory Model (JMM) — спецификация, которая описывает, при каких условиях изменения одного потока гарантированно видны другому. Без таких гарантий компилятор и процессор вольны переупорядочивать и кэшировать операции как удобно — это нормально для одного потока, но ломает многопоточный код.
Правило happens-before описывает эти гарантии: если операция A happens-before B, то все изменения A гарантированно видны B.
// Без volatile — поток 1 может вечно видеть true из своего кэша
volatile boolean running = true;
// volatile гарантирует: запись в running сразу идёт в общую память,
// а чтение — всегда из общей памяти (не из кэша ядра).
Копнуть глубже
Правила happens-before в JMM. JMM гарантирует видимость в следующих случаях:
| Ситуация | Гарантия |
|---|---|
volatile-запись → volatile-чтение | да |
выход из synchronized → вход в тот же монитор | да |
Thread.start() → любой код в потоке | да |
завершение потока → Thread.join() | да |
| порядок действий внутри одного потока | всегда |
Reordering (переупорядочивание). Компилятор JIT и процессор могут менять порядок инструкций для оптимизации. JMM запрещает переупорядочивание только через happens-before барьеры. Пример проблемы — «двойная проверка» без volatile:
// НЕПРАВИЛЬНО — объект может быть виден как ненулевой до того,
// как конструктор завершился (переупорядочивание записи поля)
private static Singleton instance;
// ПРАВИЛЬНО
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // volatile → барьер записи
}
}
}
return instance;
}
• почему volatile решает проблему видимости но не атомарности (если дошёл до 2-го слоя).