В Java-примере: class Worker implements Runnable { boolean running = true; public void run(){ while(running){ /* work */ } } public void stop(){ running = false; } } объясните, почему вызов stop() может не остановить поток на практике, какие гарантии предоставляет модель памяти Java, и как исправить код безопасно
Коротко: потому что без механизма видимости поток, выполняющий цикл, может НЕ увидеть запись из другого потока — значение может кэшироваться или компилятор/JIT может упростить цикл. Java Memory Model (JMM) даёт гарантию атомарности для простых типов (например, чтение/запись boolean атомарны), но не гарантирует видимость изменений между потоками без установленного отношения happens-before. Почему так происходит - В коде while(running)while(running)while(running) поток читает переменную многократно; если запись running=falserunning = falserunning=false из другого потока не образует happens-before с этими чтениями, читатель может всегда видеть старое значение и цикл не завершится. - JIT/CPU могут оптимизировать и «вынести» чтение переменной из цикла (loop invariant), если кажется, что переменная не меняется в текущем потоке. Что гарантирует JMM - Чтение/запись большинства примитивов (включая boolean) атомарны, длинные типы long,doublelong, doublelong,double — особый случай. - Видимость между потоками гарантирует отношение happens-before: если записывающий поток делает volatile-запись или выпускает монитор (synchronized), а читающий поток затем делает volatile-чтение или захватывает монитор — запись happens-before чтению и чтение увидит обновлённое значение. - Без happens-before поведение по видимости не гарантировано (можно продолжать видеть старое значение). Как исправить (безопасно) 1) Самый простой — сделать переменную volatile: volatile boolean running;volatile\ boolean\ running;volatilebooleanrunning; Тогда запись running=falserunning = falserunning=false из одного потока happens-before и последующие чтения в другом потоке увидят значение false, цикл завершится. 2) Использовать AtomicBoolean: AtomicBoolean running=new AtomicBoolean(true);AtomicBoolean\ running = new\ AtomicBoolean(true);AtomicBooleanrunning=newAtomicBoolean(true); Проверять running.get()running.get()running.get() в цикле и ставить running.set(false)running.set(false)running.set(false) — удобно если нужны атомарные операции типа compareAndSet. 3) Использовать прерывания и проверку флага прерывания: В рабочем потоке проверять Thread.currentThread().isInterrupted()Thread.currentThread().isInterrupted()Thread.currentThread().isInterrupted() или Thread.interrupted()Thread.interrupted()Thread.interrupted(), а управляющий поток вызывать thread.interrupt()thread.interrupt()thread.interrupt(). Это полезно, если поток может быть заблокирован в wait/sleep/io — многие блокировки бросают InterruptedException. 4) Использовать высокоуровневые средства: ExecutorService + shutdown()/shutdownNow(), Future.cancel(true) — правильнее, если потоки управляются пулом. Примеры: - volatile: class Worker implements Runnable { private volatile boolean running = true; public void run() { while (running) { /* work */ } } public void stop() { running = false; } } - AtomicBoolean: class Worker implements Runnable { private final AtomicBoolean running = new AtomicBoolean(true); public void run() { while (running.get()) { /* work */ } } public void stop() { running.set(false); } } Вывод: проблема — не атомарность, а видимость; исправлять нужно через volatile, синхронизацию, атомарные классы или правильно используемые прерывания/пулы.
Почему так происходит
- В коде while(running)while(running)while(running) поток читает переменную многократно; если запись running=falserunning = falserunning=false из другого потока не образует happens-before с этими чтениями, читатель может всегда видеть старое значение и цикл не завершится.
- JIT/CPU могут оптимизировать и «вынести» чтение переменной из цикла (loop invariant), если кажется, что переменная не меняется в текущем потоке.
Что гарантирует JMM
- Чтение/запись большинства примитивов (включая boolean) атомарны, длинные типы long,doublelong, doublelong,double — особый случай.
- Видимость между потоками гарантирует отношение happens-before: если записывающий поток делает volatile-запись или выпускает монитор (synchronized), а читающий поток затем делает volatile-чтение или захватывает монитор — запись happens-before чтению и чтение увидит обновлённое значение.
- Без happens-before поведение по видимости не гарантировано (можно продолжать видеть старое значение).
Как исправить (безопасно)
1) Самый простой — сделать переменную volatile:
volatile boolean running;volatile\ boolean\ running;volatile boolean running;
Тогда запись running=falserunning = falserunning=false из одного потока happens-before и последующие чтения в другом потоке увидят значение false, цикл завершится.
2) Использовать AtomicBoolean:
AtomicBoolean running=new AtomicBoolean(true);AtomicBoolean\ running = new\ AtomicBoolean(true);AtomicBoolean running=new AtomicBoolean(true);
Проверять running.get()running.get()running.get() в цикле и ставить running.set(false)running.set(false)running.set(false) — удобно если нужны атомарные операции типа compareAndSet.
3) Использовать прерывания и проверку флага прерывания:
В рабочем потоке проверять Thread.currentThread().isInterrupted()Thread.currentThread().isInterrupted()Thread.currentThread().isInterrupted() или Thread.interrupted()Thread.interrupted()Thread.interrupted(), а управляющий поток вызывать thread.interrupt()thread.interrupt()thread.interrupt(). Это полезно, если поток может быть заблокирован в wait/sleep/io — многие блокировки бросают InterruptedException.
4) Использовать высокоуровневые средства: ExecutorService + shutdown()/shutdownNow(), Future.cancel(true) — правильнее, если потоки управляются пулом.
Примеры:
- volatile:
class Worker implements Runnable {
private volatile boolean running = true;
public void run() {
while (running) { /* work */ }
}
public void stop() { running = false; }
}
- AtomicBoolean:
class Worker implements Runnable {
private final AtomicBoolean running = new AtomicBoolean(true);
public void run() {
while (running.get()) { /* work */ }
}
public void stop() { running.set(false); }
}
Вывод: проблема — не атомарность, а видимость; исправлять нужно через volatile, синхронизацию, атомарные классы или правильно используемые прерывания/пулы.