Разберите проблему гонки данных в многопоточном Java-коде: Runnable r = () -> { counter++; }; Thread t1=new Thread(r); Thread t2=new Thread(r); t1.start(); t2.start(); — почему counter может быть неконсистентен и как это исправить
Коротко — потому что операция counter++counter++counter++ не атомарна и есть проблемы видимости между потоками. Почему так: - counter++counter++counter++ фактически выполняется как три шага: чтение, вычисление, запись — например, read v\text{read }vread v, compute v+1\text{compute }v+1compute v+1, write v+1\text{write }v+1write v+1. При параллельном выполнении два потока могут оба прочитать одно и то же значение (скажем, 0\,00), оба записать 1\,11 — потерянное обновление: ожидаем 2\,22, получаем 1\,11. - Без синхронизации изменения одного потока могут быть не сразу видны другому (Java Memory Model) — даже если операции не перекрываются, чтение может вернуть «старое» значение. Как исправить (варианты, кратко): 1) Синхронизация: - synchronized-блок: Runnable r = () -> { synchronized(lock) { counter++; } }; - или synchronized-метод. Гарантирует атомарность и видимость. 2) Atomic-классы (рекомендуется для простого инкремента): - Использовать AtomicInteger: AtomicInteger counter = new AtomicInteger(0); Runnable r = () -> counter.incrementAndGet(); - Это атомарно и быстрее, чем общая синхронизация при низкой/средней нагрузке. 3) ReentrantLock: - Если нужен явный lock/unlock или tryLock: lock.lock(); try { counter++; } finally { lock.unlock(); } 4) LongAdder для высокой конкуренции: - LongAdder даёт лучший масштаб при большом числе потоков: LongAdder counter = new LongAdder(); counter.increment(); long value = counter.sum(); Что не поможет: - volatile int counter; — сделает видимым последнее значение, но не сделает counter++counter++counter++ атомарным (проблема потерянных обновлений останется). Дополнение: чтобы корректно прочитать итог после старта потоков, дождитесь их завершения через t1.join(); t2.join(); — иначе можете читать значение до завершения инкрементов.
Почему так:
- counter++counter++counter++ фактически выполняется как три шага: чтение, вычисление, запись — например, read v\text{read }vread v, compute v+1\text{compute }v+1compute v+1, write v+1\text{write }v+1write v+1. При параллельном выполнении два потока могут оба прочитать одно и то же значение (скажем, 0\,00), оба записать 1\,11 — потерянное обновление: ожидаем 2\,22, получаем 1\,11.
- Без синхронизации изменения одного потока могут быть не сразу видны другому (Java Memory Model) — даже если операции не перекрываются, чтение может вернуть «старое» значение.
Как исправить (варианты, кратко):
1) Синхронизация:
- synchronized-блок:
Runnable r = () -> {
synchronized(lock) { counter++; }
};
- или synchronized-метод. Гарантирует атомарность и видимость.
2) Atomic-классы (рекомендуется для простого инкремента):
- Использовать AtomicInteger:
AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
- Это атомарно и быстрее, чем общая синхронизация при низкой/средней нагрузке.
3) ReentrantLock:
- Если нужен явный lock/unlock или tryLock:
lock.lock();
try { counter++; } finally { lock.unlock(); }
4) LongAdder для высокой конкуренции:
- LongAdder даёт лучший масштаб при большом числе потоков:
LongAdder counter = new LongAdder();
counter.increment();
long value = counter.sum();
Что не поможет:
- volatile int counter; — сделает видимым последнее значение, но не сделает counter++counter++counter++ атомарным (проблема потерянных обновлений останется).
Дополнение: чтобы корректно прочитать итог после старта потоков, дождитесь их завершения через t1.join(); t2.join(); — иначе можете читать значение до завершения инкрементов.