Разберите случай гонки (race condition) в многопоточной программе на Java, где два потока инкрементируют общий счётчик без синхронизации; предложите корректные варианты исправления и оцените их накладные расходы

21 Ноя в 10:36
3 +3
0
Ответы
1
Коротко — что происходит
- При инкременте общего счётчика `counter++` выполняется последовательность: чтение, +1, запись. Это не атомарно, поэтому при 222 потоках возможна гонка: оба читают одно и то же значение и оба записывают одинаковый результат — потеря инкремента.
Правильные варианты исправления (с примерами и назначением)
1) synchronized (монитор)
- Код:
public synchronized void increment() {
counter++;
}
- Когда использовать: простая корректность, когда критическая секция небольшая или нужно защищать несколько операций одновременно.
- Накладные расходы: при отсутствии конкуренции — небольшие (время входа/выхода монитора), при конкуренции — блокировка/ожидание и парковка потоков, стоимость растёт сильно.
- Приблизительно: без конкуренции ~ 20–50\,20\text{–}502050 нс; при сильной конкуренции — от 1 μs1\ \mu s1 μs до миллисекунд и выше (зависит от платформы).
2) ReentrantLock
- Код:
lock.lock();
try { counter++; } finally { lock.unlock(); }
- Когда: нужен явный контроль (tryLock, fairness), или когда синхронизация должна быть привязана к объекту отдельно.
- Накладные расходы: аналогичны `synchronized` (внутренне сложнее), fair-режим заметно медленнее.
- Примерная производительность: близка к `synchronized` в большинстве сценариев.
3) AtomicInteger / AtomicLong (CAS)
- Код:
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
- Когда: простая атомарная операция инкремента, низкая/умеренная конкуренция, требование lock-free.
- Накладные расходы: очень малая при успехе (атомарный CAS-инструкция) — обычно несколько наносекунд; при высокой конкуренции — многократные повторы CAS (рост задержок).
- Приблизительно: успешный CAS ~ 5–20\,5\text{–}20520 нс; при частых retry стоимость растёт линейно с количеством неудачных попыток.
4) LongAdder / LongAccumulator
- Код:
LongAdder counter = new LongAdder();
counter.increment();
- Когда: большой объём конкурентных инкрементов (много потоков), требуется высокая пропускная способность для обновлений; допустимо, что чтение — чуть дороже.
- Накладные расходы: при обновлениях очень низкая конкуренция за каждую «ячейку» — высокая пропускная способность; чтение делает суммирование по всем ячейкам.
- Сложности: память для mmm ячеек, чтение — O(m)O(m)O(m) (где mmm = число ячеек, обычно ~число логических CPU).
- Подходит, если число обновлений >> число чтений.
5) CAS-цикл вручную (compareAndSet)
- Код: loop: int v = a.get(); if (a.compareAndSet(v, v+1)) break;
- Когда: если нужна специальная логика при конфликте. Обычно лучше использовать `AtomicInteger.incrementAndGet()`.
Чего не делать
- volatile не решает проблему инкремента: `volatile int x; x++` всё ещё не атомарно.
- Не нужен `synchronized` если достаточно атомарной операции (AtomicInteger/LongAdder предпочтительнее для простого счётчика).
Короткая рекомендация
- Если нужно просто атомарно инкрементировать и потоков не очень много — используйте `AtomicInteger` / `AtomicLong`.
- Если много потоков и интенсивные обновления — `LongAdder`.
- Если критическая секция сложнее одного инкремента (несколько операций) или требуется строгая последовательность — `synchronized` или `ReentrantLock`.
Если нужно — могу привести микро-бенчмарки для вашей конкретной нагрузки и JVM, чтобы оценить реальные числа.
21 Ноя в 10:43
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир