Что такое happens-before в Java Memory Model
Что проверяют
Понимаете ли вы, что без синхронизации один поток может вообще не увидеть запись другого, и знаете ли правила, которые это гарантируют. Следом обычно просят объяснить, почему double-checked locking ломается без volatile, или чем volatile отличается от synchronized.
Как отвечать
Happens-before это отношение частичного порядка из спецификации (JLS, глава 17, JSR-133). Если действие A happens-before B, всё, что записал A, гарантированно видно в B, и никакие перестановки операций этого не нарушат. Если такой связи между записью и чтением общей переменной нет, это гонка данных, и прочитать можно устаревшее значение.
Основные правила:
- порядок программы: внутри потока каждое действие happens-before следующего по коду;
- монитор: освобождение монитора (выход из
synchronized) happens-before каждого последующего захвата того же монитора; - volatile: запись в
volatile-поле happens-before каждого последующего чтения этого поля; - потоки:
Thread.start()happens-before любого действия в запущенном потоке, а все действия потока happens-before возврата изjoin()у него; - транзитивность: из A hb B и B hb C следует A hb C. Благодаря ей видны и обычные поля, записанные перед
volatile-записью.
На этом стоят гарантии java.util.concurrent: помещение в ConcurrentHashMap или BlockingQueue happens-before извлечения, countDown() у CountDownLatch happens-before возврата из await(), работа задачи happens-before возврата из Future.get(). Отдельно стоят final-поля: их видят все потоки без синхронизации, если this не утёк из конструктора.
Где спотыкаются
Путают happens-before с порядком по времени. Правило говорит о видимости, а не о том, какой поток выполнится первым: volatile не заставит поток стартовать раньше.
Формулируют правило монитора как «вход в synchronized happens-before выхода». Связь возникает между выходом одного потока и последующим входом другого по тому же объекту, синхронизация на разных объектах ничего не даёт.
Считают, что volatile делает count++ атомарным. Видимость есть, атомарности нет: чтение и запись остаются двумя операциями, нужен AtomicInteger или LongAdder.