Що таке 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 при вході/виході