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

Что можно сделать с примитивами чтобы избежать конкурентный доступ

1.8 Middle🔥 121 комментариев
#JVM и управление памятью#Многопоточность

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

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

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

Что можно сделать с примитивами чтобы избежать конкурентный доступ

Конкурентный доступ к примитивам в многопоточной среде — частая причина ошибок. Когда несколько потоков одновременно читают и записывают примитивные значения (int, long, boolean), возникают race conditions. Существует несколько стратегий для защиты от этих проблем.

Проблема: Race Condition

Без синхронизации происходит беда:

public class Counter {
    private int count = 0;  // Примитив, доступ не синхронизирован
    
    public void increment() {
        count++;  // Это НЕ атомарная операция!
    }
    
    public int getCount() {
        return count;
    }
}

// Два потока, каждый вызывает increment() 1000 раз
// Ожидаемый результат: 2000
// Реальный результат: обычно < 2000 (например, 1234, 1876 и т.д.)

Почему? Операция count++ состоит из трёх шагов:

  1. Прочитать текущее значение count
  2. Добавить 1
  3. Записать результат обратно

Если два потока делают это одновременно, их операции могут перемешаться.

Решение 1: Synchronized метод

Самое простое решение — синхронизировать доступ:

public class Counter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;  // Теперь атомарна
    }
    
    public synchronized int getCount() {
        return count;
    }
}

// Результат: всегда 2000
// Но: конкурентность низкая, другие потоки ждут

Плюсы: простой, встроенный Минусы: снижает конкурентность, все потоки ждут в очереди

Решение 2: Synchronized блок на части критического кода

Не нужно синхронизировать весь метод, если синхронизировать нужно только часть:

public class Counter {
    private int count = 0;
    private Object lock = new Object();
    
    public void doSomething() {
        // Некритичная работа, может выполняться параллельно
        long startTime = System.currentTimeMillis();
        
        synchronized(lock) {
            // Только эта часть требует синхронизации
            count++;
        }
        
        // Ещё некритичная работа
        processResult();
    }
}

Плюсы: лучше конкурентность, синхронизируем только необходимое Минусы: нужно правильно выбрать границы блока

Решение 3: Atomic классы (рекомендуется)

Java предоставляет специальные Atomic классы для примитивов:

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();  // Атомарная операция
    }
    
    public int getCount() {
        return count.get();  // Атомарное чтение
    }
}

// Результат: всегда 2000
// Конкурентность: НАМНОГО выше, чем synchronized

Доступные Atomic классы:

AtomicInteger      // Для int
AtomicLong         // Для long
AtomicBoolean      // Для boolean
AtomicReference<T> // Для объектных ссылок

Пример с AtomicLong для счётчика

public class Statistics {
    private AtomicLong totalRequests = new AtomicLong(0);
    private AtomicLong successfulRequests = new AtomicLong(0);
    private AtomicLong failedRequests = new AtomicLong(0);
    
    public void recordSuccess() {
        totalRequests.incrementAndGet();
        successfulRequests.incrementAndGet();
    }
    
    public void recordFailure() {
        totalRequests.incrementAndGet();
        failedRequests.incrementAndGet();
    }
    
    public long getTotal() {
        return totalRequests.get();
    }
    
    public double getSuccessRate() {
        long total = totalRequests.get();
        if (total == 0) return 0;
        return (double) successfulRequests.get() / total * 100;
    }
}

// Тысячи потоков могут параллельно вызывать recordSuccess() и recordFailure()
// Результаты будут корректны благодаря атомарности

Решение 4: Immutable объекты (неизменяемые)

Если объект неизменяемый, то конкурентный доступ безопасен:

public final class ImmutableCounter {
    private final int value;
    
    public ImmutableCounter(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
    
    public ImmutableCounter increment() {
        return new ImmutableCounter(value + 1);  // Создаём новый объект
    }
}

// Использование:
ImmutableCounter counter = new ImmutableCounter(0);
counter = counter.increment();  // Переменная меняется, но объект нет

Плюсы: полностью потокобезопасно, нет логирования Минусы: создание новых объектов при каждом изменении

Решение 5: Volatile для примитивов (осторожно)

Volatile гарантирует видимость изменений между потоками, но не атомарность:

public class Counter {
    private volatile int count = 0;  // Изменения видны всем потокам
    
    public void increment() {
        count++;  // ВСЕ ЕЩЁ race condition!
    }
}

// Неправильно! volatile не делает операцию атомарной

volatile работает только для чтения/записи, не для сложных операций:

public class Flag {
    private volatile boolean shutdown = false;
    
    public void shutdown() {
        shutdown = true;  // Изменение видно всем потокам сразу
    }
    
    public boolean isShutdown() {
        return shutdown;  // Всегда читаем свежее значение
    }
}

// Это ПРАВИЛЬНОЕ использование volatile

Решение 6: ThreadLocal для изоляции данных

Если каждому потоку нужны свои данные:

public class RequestContext {
    private static ThreadLocal<String> userId = new ThreadLocal<>();
    private static ThreadLocal<String> sessionId = new ThreadLocal<>();
    
    public static void setUserId(String id) {
        userId.set(id);  // Каждому потоку своё значение
    }
    
    public static String getUserId() {
        return userId.get();  // Потокобезопасно
    }
    
    public static void clear() {
        userId.remove();    // ВАЖНО: очищать после использования
        sessionId.remove();  // Иначе memory leak в thread pool
    }
}

// В servlet:
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    RequestContext.setUserId(extractUserId(req));
    try {
        // Код может вызвать RequestContext.getUserId() где угодно
        // А значение будет правильным для текущего потока
    } finally {
        RequestContext.clear();  // Очистить для переиспользования потока
    }
}

Решение 7: Передача значений вместо общего состояния

Лучший способ — избежать общего состояния вообще:

// Плохо: общее состояние
public class BadCalculator {
    private int result = 0;
    
    public void add(int a, int b) {
        result = a + b;  // Потокоопасная переменная
    }
    
    public int getResult() {
        return result;
    }
}

// Хорошо: функциональный подход
public class GoodCalculator {
    public static int add(int a, int b) {
        return a + b;  // Нет общего состояния
    }
}

// Ещё лучше: явная передача результата
public void calculate(int a, int b, Consumer<Integer> callback) {
    int result = a + b;
    callback.accept(result);
}

Сравнение подходов

ПодходКонкурентностьСложностьРекомендуется
synchronizedНизкаяНизкаяПростые случаи
AtomicВысокаяСредняяСчётчики, флаги
ImmutableОчень высокаяСредняяПередача данных
volatileСредняяНизкаяТолько флаги
ThreadLocalОчень высокаяСредняяКонтекст запроса
ФункциональныйОчень высокаяСредняяАрхитектура

Лучший практический пример

public class RateLimiter {
    private final AtomicLong requestCount = new AtomicLong(0);
    private final AtomicLong lastResetTime = new AtomicLong(System.currentTimeMillis());
    private final int maxRequestsPerSecond;
    
    public RateLimiter(int maxRequestsPerSecond) {
        this.maxRequestsPerSecond = maxRequestsPerSecond;
    }
    
    public boolean allowRequest() {
        long now = System.currentTimeMillis();
        long lastReset = lastResetTime.get();
        
        // Сбросить счётчик каждую секунду
        if (now - lastReset > 1000) {
            if (lastResetTime.compareAndSet(lastReset, now)) {
                requestCount.set(0);
            }
        }
        
        long count = requestCount.get();
        if (count < maxRequestsPerSecond) {
            requestCount.incrementAndGet();
            return true;
        }
        return false;
    }
}

// Тысячи потоков могут параллельно вызывать allowRequest()
// Никаких deadlock'ов и race condition'ов

Вывод: для примитивов в многопоточной среде используй Atomic классы (лучший выбор), synchronized блоки, volatile для флагов, либо избегай общего состояния вообще через функциональный подход или ThreadLocal для контекста.

Что можно сделать с примитивами чтобы избежать конкурентный доступ | PrepBro