Что такое happens-before relationship?
Здесь: x = 10 happens-before ready = true (program order rule: в ОДНОМ потоке операция раньше в коде видна операции позже в коде), а ready = true happens-before чтение ready в д...
Junior уровень
Базовое понимание
Happens-before — это гарантия Java о видимости результатов между потоками.
Важно: Happens-before НЕ означает, что действие A выполнится раньше B по времени. Потоки МОГУТ выполняться в любом порядке. HB лишь гарантирует: если A happens-before B, то результат A будет виден в B — как если бы A произошло раньше.
Простой пример
// Поток 1:
int x = 10; // Действие A
ready = true; // Действие B (volatile)
// Поток 2:
if (ready) { // Действие C
System.out.println(x); // Всегда выведет 10!
}
Здесь: x = 10 happens-before ready = true (program order rule: в ОДНОМ потоке операция раньше в коде видна операции позже в коде), а ready = true happens-before чтение ready в другом потоке (volatile правило). По транзитивности: x = 10 happens-before чтения в потоке 2.
Почему это важно?
Без happens-before компилятор и процессор могут переупорядочить инструкции:
// БЕЗ happens-before гарантий:
int x = 10;
boolean flag = false; // без volatile
// Поток 1: // Поток 2:
x = 10; if (flag) {
flag = true; System.out.println(x); // Может быть 0!
// Процессор может поменять порядок }
Middle уровень
8 правил Happens-Before (JSR-133)
Java Memory Model определяет 8 правил:
| # | Правило | Описание |
|---|---|---|
| 1 | Program Order | В одном потоке каждое действие HB действию, расположенному ниже в коде |
| 2 | Monitor Lock | Освобождение монитора (выход из synchronized) HB последующему захвату того же монитора |
| 3 | Volatile Variable | Запись в volatile поле HB последующему чтению того же поля |
| 4 | Thread Start | Вызов Thread.start() HB любому действию в запущенном потоке |
| 5 | Thread Termination | Любое действие в потоке HB завершению этого потока (когда join() возвращается) |
| 6 | Interruption | Вызов interrupt() HB обнаружению прерывания (isInterrupted() или InterruptedException) |
| 7 | Finalizer | Завершение конструктора HB началу finalize() метода |
| 8 | Transitivity | Если A HB B и B HB C, то A HB C |
Примеры для каждого правила
1. Program Order Rule
int a = 1; // HB
int b = a + 1; // HB
int c = b + 1; // HB — порядок гарантирован в одном потоке
2. Monitor Lock Rule
synchronized(lock) {
x = 42; // Запись внутри synchronized
} // Освобождение монитора
// В другом потоке:
synchronized(lock) {
System.out.println(x); // Видит 42 — захват того же монитора
}
3. Volatile Variable Rule
volatile boolean flag = false;
int data = 0;
// Поток 1:
data = 100; // HB (program order)
flag = true; // Volatile запись
// Поток 2:
if (flag) { // Volatile чтение — HB от записи
System.out.println(data); // Всегда 100!
}
4. Thread Start Rule
int sharedData = 50;
Thread t = new Thread(() -> {
System.out.println(sharedData); // Всегда видит 50!
});
sharedData = 100; // HB (program order) перед start()
t.start(); // start() HB всем действиям в потоке t
// HB гарантирует видимость ТОЛЬКО того, что happened-before start()
// sharedData = 100 (после start()) — НЕ видна потоку t
// sharedData = 50 (до start()) — видна потоку t
5. Thread Termination Rule
Thread t = new Thread(() -> {
result = compute(); // Действие в потоке
});
t.start();
t.join(); // join() возвращается — HB
System.out.println(result); // Видит результат!
Piggybacking (Наезд на спину)
Благодаря транзитивности, можно гарантировать видимость группы обычных переменных через одну volatile:
int x = 0, y = 0, z = 0;
volatile boolean published = false;
// Поток-писатель:
x = 10;
y = 20;
z = 30;
published = true; // Volatile запись — все предыдущие записи "едут с ней"
// Поток-читатель:
if (published) { // Volatile чтение — "подцепляет" все предыдущие записи
System.out.println(x + y + z); // Всегда 60!
}
Senior уровень
Under the Hood: Memory Fences
Каждое HB отношение реализуется через Memory Fences (барьеры памяти) на уровне процессора:
Memory Fence (барьер памяти) — то же, что Memory Barrier. Запрещает процессору переупорядочивать операции вокруг барьера.
| Тип барьера | Что запрещает | Когда вставляется |
|---|---|---|
| LoadLoad | Чтение не может быть перемещено вверх перед барьером | volatile чтение |
| StoreStore | Запись не может быть перемещена вверх перед барьером | volatile запись |
| LoadStore | Запись не может быть перемещена вверх перед барьером | volatile чтение |
| StoreLoad | Чтение не может быть перемещено вверх перед барьером (самый дорогой) | volatile запись |
На x86 архитектуре:
LoadLoad,LoadStore— бесплатно (x86 не переупорядочивает чтения)StoreStore— бесплатно (x86 не переупорядочивает записи)StoreLoad— требует инструкциюmfence(Memory Fence) на x86. Самый дорогой барьер, запрещает ЛЮБОЕ переупорядочивание вокруг него.
CPU Reordering и Out-of-Order Execution
Процессоры выполняют инструкции не по порядку для оптимизации:
// Исходный код:
A = 1; // Инструкция 1
B = 2; // Инструкция 2
flag = true; // Инструкция 3 (volatile)
// Процессор может выполнить:
flag = true; // Сначала — дешевле
A = 1; // Потом
B = 2; // Потом
Без volatile барьера другой поток может увидеть flag = true, но A и B ещё не записаны.
Производительность и Highload
Цена Memory Barriers
// Бенчмарк (примерный):
volatile запись: ~10-50 ns (зависит от архитектуры)
обычная запись: ~1 ns
synchronized: ~50-500 ns (зависит от contention)
Чем больше volatile переменных — тем больше “затыков” в конвейере CPU.
False Sharing с volatile
public class VolatilePair {
public volatile long v1; // В одной кэш-линии (64 байта)
public volatile long v2; // Запись v1 инвалидирует v2!
}
Решение — @Contended:
public class ContendedPair {
@Contended
public volatile long v1; // В разных кэш-линиях
@Contended
public volatile long v2;
}
Запуск с -XX:-RestrictContended для Java 8+.
Диагностика
JCStress (Java Concurrency Stress)
Единственный инструмент для эмпирической проверки HB:
@JCStressTest
@Outcome(id = "0", expect = ACCEPTABLE, desc = "Not published")
@Outcome(id = "42", expect = ACCEPTABLE, desc = "Published correctly")
@Outcome(id = "0", expect = FORBIDDEN, desc = "Visibility bug!")
public class HBTest {
int data = 0;
volatile boolean ready = false;
@Actor
public void actor1() {
data = 42;
ready = true;
}
@Actor
public void actor2(IntResult2 r) {
if (ready) {
r.r1 = data;
}
}
}
FindBugs/SpotBugs
Обнаруживает потенциальные нарушения JMM:
// SpotBugs найдёт это:
public class Bug {
private int data; // Не volatile
private boolean ready; // Не volatile
public void write() {
data = 42;
ready = true; // HB нет! Другой поток может не увидеть data
}
}
Best Practices
- Используйте volatile для флагов и простых publication паттернов
- Используйте synchronized для составных операций
- Используйте Atomic* для lock-free счётчиков
- final поля автоматически имеют HB после конструктора
- Избегайте ручного управления HB через Unsafe/VarHandle без крайней необходимости
- Тестируйте многопоточный код через JCStress, а не только через обычные тесты
🎯 Шпаргалка для интервью
Обязательно знать:
- Happens-before — это гарантия видимости, а не хронологического порядка выполнения
- 8 правил JMM: Program Order, Monitor Lock, Volatile Variable, Thread Start, Thread Termination, Interruption, Finalizer, Transitivity
- Благодаря транзитивности можно «прицепить» видимость обычных переменных к одной volatile (piggybacking)
- На x86 StoreLoad барьер требует
mfence— самый дорогой из всех барьеров - Без happens-before процессор и JIT могут переупорядочить инструкции
finalполя автоматически получают HB-гарантию после завершения конструктора- HB гарантирует видимость результатов: если A HB B, результат A будет виден в B
Частые уточняющие вопросы:
- Happens-before означает, что A выполнится раньше B по времени? — Нет, только что результат A будет виден в B
- Как гарантировать видимость нескольких обычных переменных? — Записать их, затем записать volatile-флаг (piggybacking через транзитивность)
- Какое правило HB покрывает synchronized? — Monitor Lock: освобождение монитора HB последующему захвату того же монитора
- Чем HB отличается от memory barrier? — HB — это гарантия на уровне JMM, memory barrier — реализация на уровне CPU
Красные флаги (НЕ говорить):
- “Happens-before означает, что операции выполняются по порядку” — нет, это про видимость
- “Достаточно volatile для всех переменных” — нет, одна volatile «тащит» за собой все предыдущие записи
- “Thread.start() не имеет отношения к HB” — имеет: start() HB всем действиям в запущенном потоке
- “На x86 все барьеры дорогие” — LoadLoad и StoreStore на x86 бесплатны
Связанные темы:
- [[1. В чём разница между synchronized и volatile]] — volatile правило как одно из HB-правил
- [[3. Что такое visibility problem]] — HB решает проблему видимости на уровне JMM
- [[4. Что такое монитор (monitor) в Java]] — Monitor Lock rule связан с synchronized
- [[5. Как работает synchronized на уровне монитора]] — memory barriers при входе/выходе