W3docs

Модель памяти Java

Модель памяти Java — когда потоки видят записи друг друга и как работает отношение happens-before.

Модель памяти Java (JMM) — это часть спецификации языка, определяющая когда один поток гарантированно видит записи другого потока. Именно она стоит за volatile, synchronized и final — и именно поэтому корректный многопоточный код выглядит именно так.

В этой главе объясняется, зачем нужна данная модель, как отношение happens-before связывает всё воедино и какой инструмент выбрать при проблемах с видимостью, атомарностью или переупорядочиванием.

Почему существует модель памяти

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

JMM определяет единственную гарантию, которая всё это регулирует: отношение happens-before. Если действие A happens-before действия B, то эффекты A видны B. Всё остальное — volatile, блокировки, final, запуск и соединение потоков — лишь способ создать ребро happens-before.

// Without synchronization, this loop may NEVER terminate:
// the reader thread can cache 'running' forever and miss the write.
static boolean running = true;          // plain field — no guarantee

void reader() { while (running) { /* spin */ } }   // may hang
void stopper() { running = false; }                 // may go unseen

Ключевое слово volatile

Объявление поля volatile делает две вещи: каждое чтение идёт из основной памяти (видимость), а запись в volatile происходит happens-before каждого последующего volatile-чтения того же поля (упорядочивание). Это не делает составные операции вроде count++ атомарными.

public class Worker {
    private volatile boolean running = true;   // visible across threads

    public void run() {
        while (running) {        // always sees the latest value
            doWork();
        }
    }

    public void stop() {
        running = false;         // guaranteed visible to run()
    }
}

Используйте volatile для одного флага или ссылки, которую читают многие потоки, а пишет один. Обращайтесь к нему, когда нужна видимость, а не взаимное исключение. Подробнее см. Java volatile.

Happens-Before: основные правила

Happens-before — это контракт, на который вы опираетесь при программировании. Вот рёбра, которые вы создаёте намеренно:

ПравилоРебро happens-before
Порядок программыКаждое действие в потоке happens-before последующих действий в том же потоке
Блокировка монитораСнятие блокировки монитора happens-before следующей блокировки того же монитора
VolatileЗапись в поле volatile happens-before каждого последующего чтения из него
Запуск потокаthread.start() happens-before любого действия в запущенном потоке
Соединение потокаВсе действия потока происходят happens-before возврата другого потока из его join()
Поля finalЗаписи в поля final в конструкторе происходят happens-before публикации объекта
// synchronized creates a happens-before edge through the same lock:
synchronized (lock) { shared = compute(); }   // unlock here ...
// ... happens-before another thread's:
synchronized (lock) { use(shared); }          // ... lock here

Атомарность против видимости

Это два разных вида проблем, и для них нужны разные инструменты. volatile решает проблему видимости, но не атомарности; synchronized и классы из java.util.concurrent.atomic решают обе проблемы для охватываемого ими участка.

ПроблемаСимптомРешение
ВидимостьПоток никогда не видит обновлённое значениеvolatile, synchronized, final
АтомарностьПотерянные обновления из-за x++ при конкуренцииsynchronized, AtomicInteger, блокировки
ПереупорядочиваниеОперации выполняются не в том порядкеhappens-before через указанные инструменты
import java.util.concurrent.atomic.AtomicLong;

public class Counter {
    private final AtomicLong hits = new AtomicLong();

    public void record() { hits.incrementAndGet(); }   // atomic + visible
    public long total()  { return hits.get(); }
}

Поля final и безопасная публикация

Поле final, установленное в конструкторе, фиксируется к моменту возврата из конструктора. Любой поток, видящий правильно сконструированный объект (ссылка на который не утекла из конструктора), гарантированно видит корректные значения его полей final — без volatile и блокировок. Именно поэтому неизменяемые объекты изначально потокобезопасны.

public final class Point {
    private final int x, y;          // frozen at construction
    public Point(int x, int y) { this.x = x; this.y = y; }
    public int x() { return x; }
    public int y() { return y; }
}
// Share a Point across threads freely: its final fields are safely published.

Самодостаточный пример

Запускаемый пример ниже использует только JDK. В нём задействованы четыре инструмента модели памяти в одной программе: volatile для видимости между потоками, AtomicInteger для счётчика без потерянных обновлений, поля final для безопасной публикации и synchronized для атомарного накопления.

java— editable, runs on the server

Что можно извлечь из выполнения:

  • reader saw data = 42 доказывает, что volatile-запись в flag опубликовала обычную запись в data — читатель гарантированно видит её благодаря ребру happens-before.
  • atomic counter = 800000 (expected 800000) показывает, что AtomicInteger.incrementAndGet() не потерял ни одного обновления при работе 8 потоков по 100 000 инкрементов каждый — обычный int++ выдал бы меньшее, недетерминированное число.
  • final config = prod:443 демонстрирует безопасную публикацию: поля final класса Config корректны без volatile или блокировок.
  • synchronized sum = 10000 подтверждает, что четыре записывающих потока (1000+2000+3000+4000) накапливали значения через один монитор без потерянных добавлений.
  • Каждая строка вывода соответствует отдельному механизму happens-before, однако они компонуются в одной программе — инструменты JMM дополняют друг друга, а не взаимозаменяемы.

Выбор правильного инструмента

Краткое руководство при выборе механизма модели памяти:

  • Один писатель, много читателей флага или ссылки? Используйте volatile.
  • Счётчики, накопители или compare-and-set для одной переменной? Используйте атомарные классы — они избегают накладных расходов на блокировки.
  • Многошаговое обновление, которое должно быть всё или ничего? Защитите его с помощью synchronized (или явной блокировки).
  • Обмен данными только для чтения? Сделайте объект неизменяемым с полями final; см. Неизменяемые классы. volatile и блокировки не нужны.

Связанные главы

Практика

Практика
Какую гарантию даёт объявление поля 'volatile' в модели памяти Java?
Какую гарантию даёт объявление поля 'volatile' в модели памяти Java?
Was this page helpful?