Проанализируйте следующий Java‑код на предмет состояния гонки и возможной потери данных при многопоточном выполнении: public class Counter{ private int c = 0; public void inc(){ c = c + 1; } public int get(){ return c; } } — какие способы синхронизации и тесты вы предложите, чтобы убедиться в корректности под высокой нагрузкой

6 Окт в 10:39
9 +2
0
Ответы
1

Код

public class Counter {
private int c = 0;
public void inc { c = c + 1; }
public int get { return c; }
}

имеет явную ситуацию гонки: инкремент c = c + 1 не является атомарной операцией (чтение -> изменение -> запись). При параллельных вызовах inc разные потоки могут читать одно и то же старое значение и перезаписывать друг друга — потеря инкрементов. Кроме того, без синхронизации нет гарантий видимости одинпотокможетнеувидетьобновление,сделанноевдругомодин поток может не увидеть обновление, сделанное в другомодинпотокможетнеувидетьобновление,сделанноевдругом.

Способы синхронизации скраткимописаниемплюсов/минусовс кратким описанием плюсов/минусовскраткимописаниемплюсов/минусов

1) synchronized

Простое решение: объявить методы synchronized.
public synchronized void inc { c = c + 1; }
public synchronized int get { return c; }Гарантирует атомарность и видимость.При высокой конкуренции может стать узким местом блокировканаодномэкземпляреблокировка на одном экземпляреблокировканаодномэкземпляре.

2) java.util.concurrent.atomic.AtomicInteger / AtomicLong

Использовать AtomicInteger и метод incrementAndGet / getAndIncrement:
private final AtomicInteger c = new AtomicInteger000;
public void inc { c.incrementAndGet; }
public int get { return c.get; }Низкоуровневая CAS-алгоритмика, лучше масштабируется, чем synchronized, для простых операций.Гарантирует атомарность и видимость.

3) java.util.concurrent.atomic.LongAdder илиLongAccumulatorили LongAccumulatorилиLongAccumulator

Хорош для очень высокой конкуренции и большой частоты инкрементов:
private final LongAdder c = new LongAdder;
public void inc { c.increment; }
public long get { return c.sum; }Существенно лучше по пропускной способности под сильной нагрузкой, потому что использует ряд частных счетчиков и суммирует их.Минус: get выполняет суммирование и может быть чуть дороже; при редком чтении — идеален. Семантика суммирования корректна: возвращается текущее значение, но под большим параллелизмом чтение может быть «поздним» относительно одновременных инкрементов обычноэтонепроблемаобычно это не проблемаобычноэтонепроблема.

4) Lock ReentrantLockReentrantLockReentrantLock

Аналогично synchronized, но с дополнительными возможностями tryLock,fairnesstryLock, fairnesstryLock,fairness:
private final ReentrantLock lock = new ReentrantLock;
public void inc {
lock.lock;
try { c = c + 1; } finally { lock.unlock; }
}

5) volatile

Помогает видимости, но не делает инкремент атомарным. Нельзя использовать один volatile int для корректного inc:
private volatile int c;
public void inc { c = c + 1; } // всё ещё небезопасно

Рекомендации по выбору

Если нужен простой корректный счетчик — использовать AtomicInteger/AtomicLong.Если ожидается очень высокая частота инкрементов и мало чтений — LongAdder.Если инкремент — часть более сложной критической секции несколькополейнужнообновитьвместенесколько полей нужно обновить вместенесколькополейнужнообновитьвместе — synchronized / ReentrantLock.

Как тестировать под высокой нагрузкой стратегии+примертестастратегии + пример тестастратегии+примертеста

Цель: обнаружить потерянные инкременты и проблемы видимости.

1) Стресс-тест с большим количеством потоков/итераций

Создайте N потоков; в каждом вызовите inc M раз.Синхронизированно стартуйте все потоки CountDownLatchCountDownLatchCountDownLatch чтобы увеличить вероятность конкуренции.Дождитесь завершения joinилидругойlatchjoin или другой latchjoinилидругойlatch.Ожидаемое итоговое значение = N * M. Если меньше — потеря данных.

Пример теста скелетскелетскелет:

int THREADS = 100;
int ITERS = 200_000;

Counter counter = new CounterImpl; // реализация, которую тестируете
ExecutorService ex = Executors.newFixedThreadPoolTHREADSTHREADSTHREADS;
CountDownLatch start = new CountDownLatch111;
CountDownLatch done = new CountDownLatchTHREADSTHREADSTHREADS;

for (int t = 0; t < THREADS; t++) {
ex.submit(() -> {
try {
start.await();
for (int i = 0; i < ITERS; i++) {
counter.inc();
// можно иногда Thread.yield() или случайный sleep(0,1) для увеличения интерливинга
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
finally { done.countDown(); }
});
}

start.countDown; // старт всех потоков
done.await; // ждём завершения
ex.shutdown;

long expected = longlonglong THREADS * ITERS;
long actual = counter.get;
if actual!=expectedactual != expectedactual!=expected {
throw new AssertionError("Lost increments: expected=" + expected + " actual=" + actual);
}

Параметры: увеличивайте THREADS и ITERS, запускайте тест многократно вциклев циклевцикле. Добавляйте случайные задержки в теле цикла, Thread.yield, System.gc между запусками — это повышает шанс обнаружить гонки.

2) Множественные повторные прогоны и статистика

Запускайте тест десятки/сотни раз, собирайте распределение результатов. Небольшие тесты могут случайно «пройти» даже с ошибкой; многократность уменьшает вероятность пропуска дефекта.

3) Инструменты для углублённого тестирования

jcstress OpenJDKJCStressOpenJDK JCStressOpenJDKJCStress — специализированный инструмент для проверки примитивных свойств под JMM reordering,visibility,atomicityreordering, visibility, atomicityreordering,visibility,atomicity. Рекомендуется для тестирования примитивных сценариев.FindBugs/SpotBugs, IntelliJ inspections — статический анализ для обнаружения очевидных незащищённых полей.ThreadSanitizer не для JVM; для native-кода. Для Java используйте jcstress и развернутые стресс‑тесты.

4) Проверка видимости

Если inc синхронизировано, а get нет, можно наблюдать устаревшие значения на других потоках. Поэтому либо оба метода должны использовать одну и ту же синхронизацию synchronized/locksynchronized/locksynchronized/lock, либо используйте атомарные типы AtomicInteger/LongAdderдаютвидимостьAtomicInteger/LongAdder дают видимостьAtomicInteger/LongAdderдаютвидимость.

5) Производительность и сравнение

Используйте JMH для бенчмарков, если нужно сравнить throughput/latency разных реализаций synchronizedvsAtomicIntegervsLongAddersynchronized vs AtomicInteger vs LongAddersynchronizedvsAtomicIntegervsLongAdder.

Дополнительные советы при тестировании

Тестировать на машине с несколькими ядрами/CPU одноядернаясреданепокажетмногихпроблемодноядерная среда не покажет многих проблемодноядернаясреданепокажетмногихпроблем.Запускать с различными версиями JVM и опциями JITможетвлиятьнаоптимизацииJIT может влиять на оптимизацииJITможетвлиятьнаоптимизации.Включать GC и фоновые нагрузки, они меняют планировщик и видимость.Делать тесты длительными минутыминутыминуты и многократными.Для поиска редких условий включайте рандомные sleeps в местах, где возможна гонка, чтобы «растянуть» окно гонки.

Итог

Текущий код небезопасен: потеря инкрементов и проблемы видимости возможны.Быстрое и корректное решение — заменить int на AtomicInteger илиLongAdderпривысокомконкурентномнагрузкеили LongAdder при высоком конкурентном нагрузкеилиLongAdderпривысокомконкурентномнагрузке.Для уверенности запускать стресс-тесты с CountDownLatch/ExecutorService, много потоков/итераций, многократно, и по возможности использовать jcstress для тонких проверок JMM.
6 Окт в 11:21
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир