← Назад к вопросам

Потокобезопасный счётчик с AtomicInteger

2.0 Middle🔥 201 комментариев
#Многопоточность#Основы Java

Условие

Реализуйте потокобезопасный счётчик, который будет инкрементироваться из нескольких потоков.

Требования

  1. Реализуйте версию с synchronized
  2. Реализуйте версию с AtomicInteger
  3. Сравните производительность обоих подходов

Тест

Запустите 1000 потоков, каждый из которых инкрементирует счётчик 1000 раз. Итоговое значение должно быть 1_000_000.

Вопросы

  1. Почему обычный int++ не работает в многопоточной среде?
  2. Что такое compare-and-swap (CAS)?
  3. Когда использовать AtomicInteger, а когда synchronized?

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Потокобезопасный счётчик с 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);

Вывод

  1. Обычный int++ не потокобезопасен в многопоточной среде из-за race condition
  2. CAS (Compare-And-Swap) — атомарная операция без блокировок, намного быстрее
  3. AtomicInteger использует CAS для потокобезопасности и примерно в 5-6 раз быстрее synchronized для простых счётчиков
  4. synchronized нужен для сложной логики, требующей координации
  5. В современной Java preferably используй Atomic классы для простых счётчиков и операций
Потокобезопасный счётчик с AtomicInteger | PrepBro