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

Какой будет результат при инкременте int переменной несколькими потоками?

2.3 Middle🔥 191 комментариев
#JVM и управление памятью#Многопоточность#Основы Java

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

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

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

Race Condition при инкременте переменной несколькими потоками

Это классический пример проблемы с многопоточностью. Инкремент i++ — это не атомарная операция, что приводит к потере данных.

Проблема: Инкремент не атомарен

Операция i++ выглядит просто, но на низком уровне состоит из трёх действий:

1. Прочитать текущее значение i (READ)
2. Увеличить значение на 1 (COMPUTE)
3. Записать результат обратно (WRITE)

Демонстрация проблемы

public class RaceConditionExample {
    private int counter = 0;  // НЕ синхронизирован!
    
    public void increment() {
        counter++;  // Это NOT ATOMIC!
    }
    
    public int getCounter() {
        return counter;
    }
    
    public static void main(String[] args) throws InterruptedException {
        RaceConditionExample example = new RaceConditionExample();
        
        // Создаём 10 потоков
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                // Каждый поток инкрементит 10000 раз
                for (int j = 0; j < 10000; j++) {
                    example.increment();
                }
            });
        }
        
        // Запускаем все потоки
        for (Thread t : threads) {
            t.start();
        }
        
        // Ждём завершения
        for (Thread t : threads) {
            t.join();
        }
        
        // Ожидаемый результат: 100000
        // Фактический результат: 87234 (или другое число < 100000)
        System.out.println("Counter: " + example.getCounter());
    }
}

Ожидаемый результат: 100000 (10 потоков × 10000 инкрементов)

Фактический результат: ~87000-90000 (число будет разным при каждом запуске)

Почему результат неправильный?

У нас есть race condition — условие гонки:

Поток 1                     Поток 2                    counter в памяти
Разницу:
i = 5 (READ)               -                          5
                            i = 5 (READ)               5
i = 6 (WRITE)              -                          6
                            i = 6 (WRITE)              6

Оба потока прочитали значение 5, оба увеличили его до 6 и записали. Один инкремент потеряется!

Решение 1: synchronized

Делаем метод синхронизированным — только один поток может выполняться одновременно:

public class SynchronizedExample {
    private int counter = 0;
    
    public synchronized void increment() {
        counter++;  // Теперь атомарно!
    }
    
    public synchronized int getCounter() {
        return counter;
    }
}

Как это работает:

  • Каждый объект имеет встроенный монитор (lock)
  • Только один поток может одновременно выполнять synchronized методы одного объекта
  • Другие потоки ждут в очереди

Недостаток: Может быть медленнее при высокой конкуренции

Решение 2: AtomicInteger (лучше всего)

Используем AtomicInteger — специальный класс для атомарных операций:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private AtomicInteger counter = new AtomicInteger(0);
    
    public void increment() {
        counter.incrementAndGet();  // Атомарная операция!
    }
    
    public int getCounter() {
        return counter.get();
    }
    
    public static void main(String[] args) throws InterruptedException {
        AtomicExample example = new AtomicExample();
        
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    example.increment();
                }
            });
        }
        
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        
        // Результат: ровно 100000
        System.out.println("Counter: " + example.getCounter());
    }
}

Преимущества:

  • Использует Compare-And-Swap (CAS) вместо блокировок
  • Лучше для высокой конкуренции
  • Не создаёт очередей ожидания

Решение 3: Lock (ReentrantLock)

Для большей гибкости используем явные блокировки:

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

public class LockExample {
    private int counter = 0;
    private final Lock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();
        try {
            counter++;
        } finally {
            lock.unlock();
        }
    }
    
    public int getCounter() {
        lock.lock();
        try {
            return counter;
        } finally {
            lock.unlock();
        }
    }
}

Решение 4: Использование volatile (не достаточно!)

ВАЖНО: volatile НЕ решает проблему для операции i++:

private volatile int counter = 0;

public void increment() {
    counter++;  // Всё ещё race condition!
}

volatile гарантирует видимость, но не атомарность. i++ по-прежнему состоит из трёх операций.

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

ПодходСкоростьПростотаКогда использовать
synchronizedСредняяПростаяПростые методы, низкая конкуренция
AtomicIntegerБыстроПростаяСчётчики, высокая конкуренция
ReentrantLockСредняяСложноСложная синхронизация, условия
volatileБыстроОчень простоТолько флаги, не операции

Итоговая ответ

При инкременте int переменной несколькими потоками БЕЗ синхронизации:

  • Результат: Неправильное (меньшее) значение
  • Причина: Race condition, i++ не атомарна
  • Решение: Использовать synchronized, AtomicInteger или Lock
  • Лучший выбор: AtomicInteger для счётчиков, synchronized для простых случаев
Какой будет результат при инкременте int переменной несколькими потоками? | PrepBro