Вопрос 2 · Раздел 9

Что такое happens-before relationship?

Здесь: x = 10 happens-before ready = true (program order rule: в ОДНОМ потоке операция раньше в коде видна операции позже в коде), а ready = true happens-before чтение ready в д...

Версии по языкам: English Russian Ukrainian

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

  1. Используйте volatile для флагов и простых publication паттернов
  2. Используйте synchronized для составных операций
  3. Используйте Atomic* для lock-free счётчиков
  4. final поля автоматически имеют HB после конструктора
  5. Избегайте ручного управления HB через Unsafe/VarHandle без крайней необходимости
  6. Тестируйте многопоточный код через 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 при входе/выходе