Проанализируйте проблему гонки в многопоточном фрагменте Java: "class Counter{ int c=0; void inc(){ c++; } }" при запуске из нескольких потоков — почему возникает некорректность, чем отличаются synchronized, ReentrantLock и AtomicInteger и когда что применять
Почему некорректность (гонка) возникает - Операция c++c++c++ не атомарна: она разбивается на чтение, вычисление и запись. Пример интерливинга двух потоков: - T1: читает c=c=c=000
- T2: читает c=c=c=000
- T1: вычисляет 0+1=0+1=0+1=111 и пишет c=c=c=111
- T2: вычисляет 0+1=0+1=0+1=111 и пишет c=c=c=111
В результате ожидаемое 222 (то есть c=c=c=222) потеряно — это классическое «lost update». Чем отличаются synchronized, ReentrantLock и AtomicInteger 1) synchronized (встроенный монитор) - Гарантирует взаимоисключение и корректную видимость памяти (happens‑before). - Блокировка и разблокировка делаются автоматически (при выходе из блока), поток блокируется и не может быть прерван при ожидании. - Поддерживает reentrancy (поток может повторно захватить тот же монитор). - Прост в использовании: - Пример: synchronized(this) { c++; } - Минусы: нет tryLock/timeout/lockInterruptibly, нельзя иметь несколько Condition‑объектов. 2) ReentrantLock (java.util.concurrent.locks.ReentrantLock) - Тоже взаимное исключение и видимость памяти; reentrant. - Больше возможностей: tryLock(), tryLock(timeout), lockInterruptibly(), можно задать fairness, несколько Condition объектов (wait/notify эквиваленты). - Нужно явно вызывать unlock() в finally. - Подходит, когда нужны расширенные возможности управления блокировкой или более гибный контролируемый порядок захвата. 3) AtomicInteger (java.util.concurrent.atomic.AtomicInteger) - Реализует неблокирующие (lock‑free) атомарные операции через CAS (compareAndSet). - Методы: getAndIncrement(), incrementAndGet(), compareAndSet(...), get(), set(). - Как правило быстрее и масштабируемее для простых операций над одним целым (счётчики, идентификаторы). - Не подходит для сложных составных операций, затрагивающих несколько переменных или требующих атомарности нескольких действий одновременно (тогда нужна внешняя блокировка). Когда что применять (правила практики) - Если требуется простая безопасная операция счётчика/индентификатора — используйте AtomicInteger. - Если нужно защитить небольшую критическую секцию из нескольких операций над связанными данными — используйте synchronized (если функционал прост). - Если нужны расширенные функции (tryLock, timeout, interruptible locking, fairness, Condition) или более тонкий контроль над блокировкой — используйте ReentrantLock. - Для атомарных комплексных инвариантов, охватывающих несколько полей — используйте блокировку (synchronized или ReentrantLock), либо продумывайте lock‑free алгоритмы (сложно). Короткое резюме - Проблема = c++c++c++ не атомарен → возможны потерянные обновления. - AtomicInteger — лучший выбор для простых счётчиков. - synchronized — простая и безопасная опция для критических секций. - ReentrantLock — когда нужны дополнительные возможности управления блокировкой.
- Операция c++c++c++ не атомарна: она разбивается на чтение, вычисление и запись. Пример интерливинга двух потоков:
- T1: читает c=c=c= 000 - T2: читает c=c=c= 000 - T1: вычисляет 0+1=0+1=0+1= 111 и пишет c=c=c= 111 - T2: вычисляет 0+1=0+1=0+1= 111 и пишет c=c=c= 111 В результате ожидаемое 222 (то есть c=c=c= 222) потеряно — это классическое «lost update».
Чем отличаются synchronized, ReentrantLock и AtomicInteger
1) synchronized (встроенный монитор)
- Гарантирует взаимоисключение и корректную видимость памяти (happens‑before).
- Блокировка и разблокировка делаются автоматически (при выходе из блока), поток блокируется и не может быть прерван при ожидании.
- Поддерживает reentrancy (поток может повторно захватить тот же монитор).
- Прост в использовании:
- Пример:
synchronized(this) { c++; }
- Минусы: нет tryLock/timeout/lockInterruptibly, нельзя иметь несколько Condition‑объектов.
2) ReentrantLock (java.util.concurrent.locks.ReentrantLock)
- Тоже взаимное исключение и видимость памяти; reentrant.
- Больше возможностей: tryLock(), tryLock(timeout), lockInterruptibly(), можно задать fairness, несколько Condition объектов (wait/notify эквиваленты).
- Нужно явно вызывать unlock() в finally.
- Подходит, когда нужны расширенные возможности управления блокировкой или более гибный контролируемый порядок захвата.
3) AtomicInteger (java.util.concurrent.atomic.AtomicInteger)
- Реализует неблокирующие (lock‑free) атомарные операции через CAS (compareAndSet).
- Методы: getAndIncrement(), incrementAndGet(), compareAndSet(...), get(), set().
- Как правило быстрее и масштабируемее для простых операций над одним целым (счётчики, идентификаторы).
- Не подходит для сложных составных операций, затрагивающих несколько переменных или требующих атомарности нескольких действий одновременно (тогда нужна внешняя блокировка).
Когда что применять (правила практики)
- Если требуется простая безопасная операция счётчика/индентификатора — используйте AtomicInteger.
- Если нужно защитить небольшую критическую секцию из нескольких операций над связанными данными — используйте synchronized (если функционал прост).
- Если нужны расширенные функции (tryLock, timeout, interruptible locking, fairness, Condition) или более тонкий контроль над блокировкой — используйте ReentrantLock.
- Для атомарных комплексных инвариантов, охватывающих несколько полей — используйте блокировку (synchronized или ReentrantLock), либо продумывайте lock‑free алгоритмы (сложно).
Короткое резюме
- Проблема = c++c++c++ не атомарен → возможны потерянные обновления.
- AtomicInteger — лучший выбор для простых счётчиков.
- synchronized — простая и безопасная опция для критических секций.
- ReentrantLock — когда нужны дополнительные возможности управления блокировкой.