Разберите следующий Java‑фрагмент многопоточного кода: class Flag { boolean done = false; void set(){ done = true; } boolean isDone(){ return done; } } Runnable r = ()->{ while(!f.isDone()){} }; Объясните, почему цикл может никогда не увидеть изменение done, и как это исправить (volatile, synchronized, AtomicBoolean), включая плюсы и минусы
Почему так происходит — коротко: - В Java операция чтения/записи поля без синхронизации не гарантирует видимость между потоками: JVM/процессор могут кэшировать значение или переставлять операции. Без "happens‑before" чтения в цикле могут постоянно видеть старое значение — JIT даже может «вынести» чтение из цикла и превратить его в бесконечный цикл. Это следствие модели памяти (JMM): при наличии data race видимости не гарантируются. Как это исправить (варианты, плюсы/минусы): volatile - Что делать: объявить поле как `volatile` — `volatile boolean done;` - Семантика: запись в volatile происходит‑before последующих чтений этого volatile в других потоках → видимость и запрет некоторых перестановок. - Плюсы: просто, низкая задержка, не блокирует, подходит для флага (одно поле). - Минусы: не даёт атомарности для составных операций (инкремент и т.п.), не подходит для согласованного обновления нескольких полей; всё ещё энергозатратно при активном busy‑spin. synchronized - Что делать: синхронизировать чтение и запись на одном мониторе: `synchronized(this){ done = true; }` и `synchronized(this){ return done; }` - Семантика: монитор release/acquire устанавливает happens‑before; кроме видимости, даёт взаимное исключение. - Плюсы: защищает сложные/несколько полей одновременно, гарантирует согласованность и атомарность блоков. - Минусы: дороже по затратам (блокировка/контенция), возможны задержки/блокировки, сложнее, чем простое volatile, если нужен только флаг. AtomicBoolean - Что делать: `AtomicBoolean done = new AtomicBoolean(false); done.set(true); done.get();` - Семантика: предоставляет атомарные операции (get/set/compareAndSet) с видимостью; под капотом используют volatile + CAS. - Плюсы: неблокирующий, подходит если нужен CAS или атомарные обновления; проще чем писать synchronized для атомики. - Минусы: чуть больше накладных расходов чем простое volatile для простого get/set; CAS может деградировать под сильной конкуренцией, но обычно предпочтительнее блокировок. Дополнительно — про busy‑wait: - Сам по себе цикл `while(!f.isDone()) {}` сильно грузит CPU. Лучше использовать блокирующие механизмы: `wait/notify`, `CountDownLatch`, `Semaphore`, `LockSupport.park/unpark` или `Object.wait()`/`notify()` — они дают и видимость (если правильно синхронизированы) и не тратят CPU. Короткое резюме: - Если нужен просто флаг — `volatile` достаточно и простое решение. - Если нужны атомарные/CAS‑операции — `AtomicBoolean`. - Если надо защищать несколько полей или выполнять критические секции — `synchronized`. - Для ожидания без спиннинга — используйте блокирующие конструкции (CountDownLatch и т.п.).
- В Java операция чтения/записи поля без синхронизации не гарантирует видимость между потоками: JVM/процессор могут кэшировать значение или переставлять операции. Без "happens‑before" чтения в цикле могут постоянно видеть старое значение — JIT даже может «вынести» чтение из цикла и превратить его в бесконечный цикл. Это следствие модели памяти (JMM): при наличии data race видимости не гарантируются.
Как это исправить (варианты, плюсы/минусы):
volatile
- Что делать: объявить поле как `volatile` — `volatile boolean done;`
- Семантика: запись в volatile происходит‑before последующих чтений этого volatile в других потоках → видимость и запрет некоторых перестановок.
- Плюсы: просто, низкая задержка, не блокирует, подходит для флага (одно поле).
- Минусы: не даёт атомарности для составных операций (инкремент и т.п.), не подходит для согласованного обновления нескольких полей; всё ещё энергозатратно при активном busy‑spin.
synchronized
- Что делать: синхронизировать чтение и запись на одном мониторе:
`synchronized(this){ done = true; }` и `synchronized(this){ return done; }`
- Семантика: монитор release/acquire устанавливает happens‑before; кроме видимости, даёт взаимное исключение.
- Плюсы: защищает сложные/несколько полей одновременно, гарантирует согласованность и атомарность блоков.
- Минусы: дороже по затратам (блокировка/контенция), возможны задержки/блокировки, сложнее, чем простое volatile, если нужен только флаг.
AtomicBoolean
- Что делать: `AtomicBoolean done = new AtomicBoolean(false); done.set(true); done.get();`
- Семантика: предоставляет атомарные операции (get/set/compareAndSet) с видимостью; под капотом используют volatile + CAS.
- Плюсы: неблокирующий, подходит если нужен CAS или атомарные обновления; проще чем писать synchronized для атомики.
- Минусы: чуть больше накладных расходов чем простое volatile для простого get/set; CAS может деградировать под сильной конкуренцией, но обычно предпочтительнее блокировок.
Дополнительно — про busy‑wait:
- Сам по себе цикл `while(!f.isDone()) {}` сильно грузит CPU. Лучше использовать блокирующие механизмы: `wait/notify`, `CountDownLatch`, `Semaphore`, `LockSupport.park/unpark` или `Object.wait()`/`notify()` — они дают и видимость (если правильно синхронизированы) и не тратят CPU.
Короткое резюме:
- Если нужен просто флаг — `volatile` достаточно и простое решение.
- Если нужны атомарные/CAS‑операции — `AtomicBoolean`.
- Если надо защищать несколько полей или выполнять критические секции — `synchronized`.
- Для ожидания без спиннинга — используйте блокирующие конструкции (CountDownLatch и т.п.).