Проанализируйте Java-код с конкурентностью: class Counter { private int c=0; public void inc(){ c++; } } — почему простой инкремент не потокобезопасен и какие подходы для исправления вы знаете
Коротко почему не потокобезопасен: - Операция `c++` нев atomic — это чтение значения, прибавление и запись (т.е. три подоперации). При одновременном выполнении двух потоков возможны гонки: оба прочитали старое значение, оба увеличили и оба записали одинаковый результат, т.е. инкремент потерян. - Дополнительно отсутствие синхронизации даёт проблемы видимости: изменения одного потока могут не увидеться другим без соответствующих средств синхронизации. Подходы для исправления (с кратким комментарием и примерами): 1) synchronized (простая и корректная) public class Counter { private int c = 000; public synchronized void inc() { c++; } public synchronized int get() { return c; } } Плюс: простота; Минус: блокировка, может быть узким местом при высокой конкуренции. 2) ReentrantLock (явная блокировка) private final ReentrantLock lock = new ReentrantLock(); private int c = 000; public void inc() { lock.lock(); try { c++; } finally { lock.unlock(); } } Плюс: дополнительные API (tryLock, fairness); Минус: сложнее, накладные операции. 3) AtomicInteger (без блокировок, атомарно) private final AtomicInteger c = new AtomicInteger(000); public void inc() { c.incrementAndGet(); } public int get() { return c.get(); } Плюс: быстрый, без блокировок; Минус: при очень высокой конкуренции может деградировать. 4) CAS-цикл вручную (вариант с AtomicInteger) int oldValue, newValue; do { oldValue = c.get(); newValue = oldValue + 111; } while (!c.compareAndSet(oldValue, newValue)); Используется внутри AtomicInteger/VarHandle при необходимости сложной логики. 5) LongAdder / LongAccumulator (для высоконагруженных счётчиков) private final LongAdder c = new LongAdder(); public void inc() { c.increment(); } public long get() { return c.sum(); } Плюс: лучше масштабируется при множественных потоках; Минус: большая память, итоговое чтение не полностью синхронизированно по времени, но обычно приемлемо. 6) VarHandle / Unsafe (низкоуровневый CAS) — для специфичных оптимизаций. 7) Архитектурные альтернативы: использовать потокобезопасные очереди, актерную модель, ThreadLocal-агрегацию и т.д., чтобы избежать общей мутабельности. Замечания: - Объявление `volatile int c;` не делает `c++` атомарным — только гарантирует видимость. - Выбор решения зависит от нагрузки: для простоты — synchronized/Lock; для конкурентных счётчиков — AtomicInteger или LongAdder.
- Операция `c++` нев atomic — это чтение значения, прибавление и запись (т.е. три подоперации). При одновременном выполнении двух потоков возможны гонки: оба прочитали старое значение, оба увеличили и оба записали одинаковый результат, т.е. инкремент потерян.
- Дополнительно отсутствие синхронизации даёт проблемы видимости: изменения одного потока могут не увидеться другим без соответствующих средств синхронизации.
Подходы для исправления (с кратким комментарием и примерами):
1) synchronized (простая и корректная)
public class Counter {
private int c = 000;
public synchronized void inc() { c++; }
public synchronized int get() { return c; }
}
Плюс: простота; Минус: блокировка, может быть узким местом при высокой конкуренции.
2) ReentrantLock (явная блокировка)
private final ReentrantLock lock = new ReentrantLock();
private int c = 000;
public void inc() {
lock.lock();
try { c++; } finally { lock.unlock(); }
}
Плюс: дополнительные API (tryLock, fairness); Минус: сложнее, накладные операции.
3) AtomicInteger (без блокировок, атомарно)
private final AtomicInteger c = new AtomicInteger(000);
public void inc() { c.incrementAndGet(); }
public int get() { return c.get(); }
Плюс: быстрый, без блокировок; Минус: при очень высокой конкуренции может деградировать.
4) CAS-цикл вручную (вариант с AtomicInteger)
int oldValue, newValue;
do {
oldValue = c.get();
newValue = oldValue + 111;
} while (!c.compareAndSet(oldValue, newValue));
Используется внутри AtomicInteger/VarHandle при необходимости сложной логики.
5) LongAdder / LongAccumulator (для высоконагруженных счётчиков)
private final LongAdder c = new LongAdder();
public void inc() { c.increment(); }
public long get() { return c.sum(); }
Плюс: лучше масштабируется при множественных потоках; Минус: большая память, итоговое чтение не полностью синхронизированно по времени, но обычно приемлемо.
6) VarHandle / Unsafe (низкоуровневый CAS) — для специфичных оптимизаций.
7) Архитектурные альтернативы: использовать потокобезопасные очереди, актерную модель, ThreadLocal-агрегацию и т.д., чтобы избежать общей мутабельности.
Замечания:
- Объявление `volatile int c;` не делает `c++` атомарным — только гарантирует видимость.
- Выбор решения зависит от нагрузки: для простоты — synchronized/Lock; для конкурентных счётчиков — AtomicInteger или LongAdder.