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

Чем будешь пользоваться при реализации потокобезопасного счетчика?

3.0 Senior🔥 191 комментариев
#Многопоточность

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

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

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

Реализация потокобезопасного счетчика: выбор инструмента

Краткий ответ

Выбор зависит от паттерна использования:

  • Высокая конкуренция, частые инкременты? AtomicLong или LongAdder
  • Просто need synchronization? synchronized или Lock
  • Нужны сложные операции? synchronized или ReadWriteLock

Инструмент 1: synchronized (простой подход)

// Базовый потокобезопасный счетчик
public class SynchronizedCounter {
    private long value;
    
    public synchronized void increment() {
        value++;
    }
    
    public synchronized long getValue() {
        return value;
    }
}

// Плюсы:
// + Простой и понятный
// + Встроен в язык
// + Автоматически управляет блокировкой

// Минусы:
// - При высокой конкуренции становится bottleneck
// - Все потоки ждут одну блокировку
// - Нет fine-grained контроля

Когда использовать:

  • Низкая конкуренция (< 10 потоков)
  • Простые операции (increment, get)
  • Не критична производительность

Инструмент 2: AtomicLong (CAS операции)

// Оптимизированный потокобезопасный счетчик
import java.util.concurrent.atomic.AtomicLong;

public class AtomicCounter {
    private final AtomicLong value = new AtomicLong(0);
    
    public void increment() {
        value.incrementAndGet();  // Compare-And-Swap (CAS)
    }
    
    public long getValue() {
        return value.get();
    }
}

// Как работает AtomicLong:
// 1. Прочитать текущее значение (100)
// 2. Вычислить новое (101)
// 3. CAS операция: atomic swap если значение ещё 100
// 4. Если другой поток изменил -> повторить

// Плюсы:
// + Нет явной блокировки (lock-free)
// + Лучше масштабируется чем synchronized
// + Работает на уровне CPU (compareAndSet)

// Минусы:
// - При очень высокой конкуренции повторяет операции
// - Сложнее для комплексных операций
// - CPU spinning может быть неэффективен

Когда использовать:

  • Средняя конкуренция (10-100 потоков)
  • Часто читаем значение
  • Операции не требуют сложную логику
// Пример с AtomicLong
public class RequestCounter {
    private final AtomicLong totalRequests = new AtomicLong();
    private final AtomicLong errorCount = new AtomicLong();
    
    public void recordRequest(boolean success) {
        totalRequests.incrementAndGet();
        if (!success) {
            errorCount.incrementAndGet();
        }
    }
    
    public long getErrorRate() {
        long total = totalRequests.get();
        long errors = errorCount.get();
        return total > 0 ? (errors * 100) / total : 0;
    }
}

Инструмент 3: LongAdder (striped лучше чем CAS)

import java.util.concurrent.atomic.LongAdder;

public class AdderCounter {
    private final LongAdder value = new LongAdder();
    
    public void increment() {
        value.increment();  // Автоматически распределяет нагрузку
    }
    
    public long getValue() {
        return value.sum();  // Суммирует все "stripes"
    }
}

// Как работает LongAdder:
// - Вместо одного счетчика, есть несколько (stripes)
// - Каждый поток пишет в свой stripe
// - Чтение суммирует все stripes
// - Меньше конкуренции за один stripe

/*
Без LongAdder:
   Thread1 ──┐
   Thread2 ──┼─→ [value] ← конкуренция!
   Thread3 ──┤
   Thread4 ──┘

С LongAdder (4 stripes):
   Thread1 → [cell_0]
   Thread2 → [cell_1]   ← нет конкуренции
   Thread3 → [cell_2]
   Thread4 → [cell_3]
*/

// Плюсы:
// + Лучше масштабируется при высокой конкуренции
// + Отличная производительность (10x+ чем AtomicLong)
// + Автоматическое распределение

// Минусы:
// - Больше памяти (несколько cells)
// - Чтение медленнее (нужно суммировать)
// - Eventual consistency (значение может быть outdated)

Когда использовать:

  • Высокая конкуренция (100+ потоков)
  • Много инкрементов, редко читаем
  • Нужна максимальная производительность

Инструмент 4: Lock / ReentrantReadWriteLock (сложные операции)

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

// Для сложных операций над счетчиком
public class LockBasedCounter {
    private long value;
    private final Lock lock = new ReentrantLock();
    
    public void incrementIfBelow(long limit) {
        lock.lock();
        try {
            if (value < limit) {  // Условная логика
                value++;
            }
        } finally {
            lock.unlock();
        }
    }
    
    public long getValue() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }
}

// С ReadWriteLock для оптимизации чтения
public class ReadWriteCounter {
    private long value;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void increment() {
        lock.writeLock().lock();
        try {
            value++;
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public long getValue() {
        lock.readLock().lock();  // Несколько потоков могут читать одновременно
        try {
            return value;
        } finally {
            lock.readLock().unlock();
        }
    }
}

Когда использовать:

  • Комплексные операции (условия, проверки)
  • Много чтения, мало записи (ReadWriteLock)
  • Нужен fine-grained контроль

Сравнение производительности

                    Low Contention    Medium Contention   High Contention
                    (4 threads)       (16 threads)        (64 threads)

synchronized        ████████          ██████████████      ████████████████████
AtomicLong          ████              ████████            ████████████
LongAdder           ███               ███                 ███
ReentrantLock       █████             ███████             ██████████

Вывод: LongAdder побеждает при высокой конкуренции

Практический пример: выбор инструмента

// Сценарий 1: REST API счетчик запросов
public class MetricsService {
    // 10000 RPS, много инкрементов, редко читаем
    private final LongAdder requestCount = new LongAdder();
    
    public void recordRequest() {
        requestCount.increment();
    }
    
    @Scheduled(fixedRate = 60000)  // каждую минуту
    public void reportMetrics() {
        log.info("Requests per minute: {}", requestCount.sum());
    }
}

// Сценарий 2: Банковский счет (редкие операции)
public class BankAccount {
    private long balance;
    private final Object lock = new Object();
    
    public synchronized void deposit(long amount) {  // synchronized достаточно
        balance += amount;
    }
    
    public synchronized boolean withdraw(long amount) {
        if (balance >= amount) {
            balance -= amount;
            return true;
        }
        return false;
    }
}

// Сценарий 3: Ограничения rate limiting
public class RateLimiter {
    private final AtomicLong tokens = new AtomicLong(MAX_TOKENS);
    
    public boolean tryConsume() {
        while (true) {
            long current = tokens.get();
            if (current <= 0) return false;
            if (tokens.compareAndSet(current, current - 1)) {
                return true;
            }
            // retry if CAS failed
        }
    }
}

// Сценарий 4: Сложная статистика
public class Statistics {
    private long sum;
    private long count;
    private double average;
    private final Lock lock = new ReentrantLock();
    
    public void addValue(long value) {
        lock.lock();
        try {
            sum += value;
            count++;
            average = (double) sum / count;  // Комплексное вычисление
        } finally {
            lock.unlock();
        }
    }
}

Рекомендация для собеседования

Если спрашивают: "Как реализовать потокобезопасный счетчик?"

Ответ:

1. Начинаю с synchronized (просто и понятно)
2. Если performance критична → AtomicLong
3. Если высокая конкуренция → LongAdder
4. Если нужна сложная логика → Lock/ReadWriteLock

Выбор зависит от:
- Частоты конкуренции
- Паттерна read/write
- Сложности операций

Итоговая таблица

ИнструментКонкуренцияПроизводительностьСложностьЛучше всего для
synchronizedНизкаяСредняяПростаяБазовые счетчики
AtomicLongСредняяХорошаяСредняяCounters, flags
LongAdderВысокаяОтличнаяСредняяMetrics, statistics
LockЛюбаяКонтролируемаяСложнаяComplex operations
Чем будешь пользоваться при реализации потокобезопасного счетчика? | PrepBro