Потокобезопасный счётчик с AtomicInteger
Условие
Реализуйте потокобезопасный счётчик, который будет инкрементироваться из нескольких потоков.
Требования
- Реализуйте версию с synchronized
- Реализуйте версию с AtomicInteger
- Сравните производительность обоих подходов
Тест
Запустите 1000 потоков, каждый из которых инкрементирует счётчик 1000 раз. Итоговое значение должно быть 1_000_000.
Вопросы
- Почему обычный int++ не работает в многопоточной среде?
- Что такое compare-and-swap (CAS)?
- Когда использовать AtomicInteger, а когда synchronized?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Потокобезопасный счётчик с AtomicInteger
Проблема обычного int++ в многопоточной среде
Операция counter++ выглядит как одна команда, но на самом деле это три операции:
// counter++; эквивалентно:
int temp = counter; // 1. Читаем значение (READ)
temp = temp + 1; // 2. Увеличиваем (COMPUTE)
counter = temp; // 3. Записываем (WRITE)
Проблема: race condition
Когда несколько потоков выполняют эти операции одновременно:
Поток 1: READ(100) -> COMPUTE(101) -> WRITE(101)
Поток 2: READ(100) -> COMPUTE(101) -> WRITE(101)
Поток 3: READ(100) -> COMPUTE(101) -> WRITE(101)
Желаемый результат: 103
Фактический результат: 101 (потеряли 2 инкремента!)
Это потому, что все потоки прочитали значение 100 перед тем, как кто-то его изменил.
Решение 1: synchronized
public class SynchronizedCounter {
private int counter = 0;
// synchronized блокирует доступ — только один поток за раз
public synchronized void increment() {
counter++; // Теперь безопасно
}
public synchronized int getValue() {
return counter;
}
}
// Тест
public class SynchronizedCounterTest {
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
// Запускаем 1000 потоков
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
// Каждый поток инкрементирует 1000 раз
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// Ждём все потоки
for (Thread t : threads) {
t.join();
}
System.out.println("Итог: " + counter.getValue()); // 1000000
}
}
Как работает synchronized:
public synchronized void increment() {
counter++; // Можно рассмотреть как:
}
// Эквивалентно:
public void increment() {
synchronized(this) { // Захватываем монитор объекта
counter++; // Только один поток за раз
} // Отпускаем монитор
}
Проблемы synchronized:
- Блокирует весь поток (даже если нужна только одна операция)
- Может привести к deadlock'ам
- Медленнее на многопроцессорных системах
- Конкурирующие потоки должны ждать
Решение 2: AtomicInteger (рекомендуется)
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
// AtomicInteger — потокобезопасная обёртка над int
private AtomicInteger counter = new AtomicInteger(0);
// Не нужен synchronized!
public void increment() {
counter.incrementAndGet(); // Атомарная операция
}
public int getValue() {
return counter.get(); // Атомарное чтение
}
}
// Тест
public class AtomicCounterTest {
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("Итог: " + counter.getValue()); // 1000000
}
}
Методы AtomicInteger:
AtomicInteger ai = new AtomicInteger(10);
// Получить значение
int value = ai.get(); // 10
// Установить значение
ai.set(20); // 20
// Инкремент
ai.incrementAndGet(); // 21 (возвращает новое значение)
ai.getAndIncrement(); // 21 (возвращает старое значение, потом инкрементирует)
// Декремент
ai.decrementAndGet(); // 20
ai.getAndDecrement(); // 20
// Добавить значение
ai.addAndGet(5); // 25 (возвращает новое значение)
ai.getAndAdd(5); // 25 (возвращает старое значение)
// Compare-And-Swap (CAS)
boolean success = ai.compareAndSet(25, 100);
// Если текущее значение == 25, установи 100 и вернись true
// Иначе вернись false (не изменится)
Что такое Compare-And-Swap (CAS)?
CAS — это атомарная операция на уровне процессора (не требует блокировок):
CAS(адрес памяти, ожидаемое значение, новое значение) {
if (памяти[адрес] == ожидаемое) {
памяти[адрес] = новое;
return true; // успешно
} else {
return false; // неудачно, попробуй ещё
}
}
Пример работы CAS:
AtomicInteger counter = new AtomicInteger(0);
// compareAndSet реализует CAS
boolean success = counter.compareAndSet(0, 1);
// Если счётчик == 0, установи 1 и вернись true
// Это используется для оптимистичной блокировки
int current;
do {
current = counter.get();
} while (!counter.compareAndSet(current, current + 1));
// Пока не сможем успешно установить значение
Почему CAS быстрее синхронизации:
synchronized:
Поток 1: захватить монитор (дорого) -> выполнить -> отпустить монитор
Поток 2: ждёт... ждёт... ждёт... -> захватить -> выполнить -> отпустить
Поток 3: ждёт...
CAS (lock-free):
Поток 1: пытается CAS -> успешно -> продолжает
Поток 2: пытается CAS -> успешно -> продолжает
Поток 3: пытается CAS -> успешно -> продолжает
Если конфликт:
Поток 1: пытается CAS -> успешно
Поток 2: пытается CAS -> неудачно -> повторяет попытку -> успешно
(не блокируется, не переходит в ожидание)
Сравнение производительности
import java.util.concurrent.atomic.AtomicInteger;
public class CounterPerformanceTest {
// Synchronized версия
static class SyncCounter {
private int counter = 0;
public synchronized void increment() { counter++; }
public synchronized int get() { return counter; }
}
// AtomicInteger версия
static class AtomicCounter {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() { counter.incrementAndGet(); }
public int get() { return counter.get(); }
}
public static void testCounter(String name, Runnable incrementer, int numThreads, int iterationsPerThread) throws InterruptedException {
long start = System.nanoTime();
Thread[] threads = new Thread[numThreads];
for (int i = 0; i < numThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < iterationsPerThread; j++) {
incrementer.run();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
long duration = System.nanoTime() - start;
System.out.println(name + ": " + (duration / 1_000_000) + " ms");
}
public static void main(String[] args) throws InterruptedException {
int numThreads = 100;
int iterationsPerThread = 10000;
// Тест synchronized
SyncCounter syncCounter = new SyncCounter();
testCounter("Synchronized", syncCounter::increment, numThreads, iterationsPerThread);
System.out.println("Результат: " + syncCounter.get());
// Тест AtomicInteger
AtomicCounter atomicCounter = new AtomicCounter();
testCounter("AtomicInteger", atomicCounter::increment, numThreads, iterationsPerThread);
System.out.println("Результат: " + atomicCounter.get());
}
}
// Типичный результат:
// Synchronized: 450 ms
// AtomicInteger: 80 ms
// AtomicInteger примерно в 5-6 раз быстрее!
Когда использовать какое решение
Используй synchronized когда:
- Нужна сложная логика (несколько переменных должны изменяться вместе)
- Нужен контроль над тем, кто может входить в критическую секцию
- Совместимость с Java 1.4 (AtomicInteger появился в Java 5)
// Пример: изменяем несколько переменных атомарно
public class Account {
private double balance;
private int transactionCount;
public synchronized void transfer(double amount) {
// Обе переменные должны измениться вместе
balance += amount;
transactionCount++;
}
}
Используй AtomicInteger когда:
- Простая операция над одним значением
- Нужна высокая производительность
- Множество потоков конкурируют за доступ
- Не нужна координация с другими переменными
// Пример: простой счётчик
public class Counter {
private AtomicInteger count = new AtomicInteger();
public void increment() { count.incrementAndGet(); }
public int get() { return count.get(); }
}
Используй ReentrantReadWriteLock когда:
- Много читателей, мало писателей
- Нужна гранулярная синхронизация
public class ReadWriteCounter {
private int counter = 0;
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void increment() {
lock.writeLock().lock();
try {
counter++;
} finally {
lock.writeLock().unlock();
}
}
public int read() {
lock.readLock().lock();
try {
return counter;
} finally {
lock.readLock().unlock();
}
}
}
Другие Atomic классы
// Основные
AtomicInteger // для int
AtomicLong // для long
AtomicBoolean // для boolean
AtomicReference<T> // для объектов
// Массивы
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray<T>
// С полями
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
Pример:
private volatile int field; // Должно быть volatile!
AtomicIntegerFieldUpdater<MyClass> updater =
AtomicIntegerFieldUpdater.newUpdater(MyClass.class, "field");
updater.incrementAndGet(this);
Вывод
- Обычный int++ не потокобезопасен в многопоточной среде из-за race condition
- CAS (Compare-And-Swap) — атомарная операция без блокировок, намного быстрее
- AtomicInteger использует CAS для потокобезопасности и примерно в 5-6 раз быстрее synchronized для простых счётчиков
- synchronized нужен для сложной логики, требующей координации
- В современной Java preferably используй Atomic классы для простых счётчиков и операций