Модель памяти 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 для атомарного накопления.
Что можно извлечь из выполнения:
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и блокировки не нужны.
Связанные главы
- Java
volatile— ключевое слово видимости подробно. - Java Synchronization — взаимное исключение и блокировка монитора.
- Atomic Variables — атомарные операции без блокировок.
finalKeyword и Immutable Classes — безопасная публикация по конструкции.