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

Является ли инкремент атомарной операцией?

2.2 Middle🔥 221 комментариев
#Многопоточность

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

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

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

Является ли инкремент атомарной операцией?

Ответ: Нет, инкремент (++) НЕ является атомарной операцией в Java, несмотря на кажущуюся простоту.

Это критически важный вопрос для многопоточных приложений, который часто приводит к ошибкам.

Почему инкремент неатомарен

На самом деле операция i++ состоит из ТРЁХ отдельных шагов:

  1. Чтение текущего значения переменной
  2. Увеличение значения на 1
  3. Запись нового значения обратно
i++ состоит из:
1. temp = i;      // Read
2. temp = temp + 1; // Compute
3. i = temp;      // Write

Этот процесс НЕ атомарен, то есть может быть прерван между шагами.

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

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

Почему получилось 9123 вместо 10000?

Поток 1: Read(5) -> Compute(6) -> [ПЕРЕКЛЮЧЕНИЕ] Поток 2: Read(5) -> Compute(6) -> Write(6) [теперь counter = 6] Поток 1: Write(6) [перезаписывает то, что написал Поток 2]

Итог: вместо 7 получилось 6. Операция Потока 2 потеряна!

Race Condition

Это называется race condition — конкурентная борьба потоков за общий ресурс.

Поток A                    counter                    Поток B
                         (value = 5)
Read(5) ────────────────────────────────────────────>
                                                   Read(5)
                         (value = 5)
Compute(6) ───────────────────────────────────────>
                                                   Compute(6)
Write(6) ──────────────────────────────────────────>
                         (value = 6)
                                                   Write(6)
                         (value = 6) ← ПОТЕРЯ!

Операция Потока A потеряна, потому что Поток B прочитал и перезаписал то же значение.

Решение 1: synchronized

public class SynchronizedCounter {
    private int counter = 0;
    
    // synchronized гарантирует атомарность
    public synchronized void increment() {
        counter++;
    }
    
    public synchronized int getCounter() {
        return counter;
    }
    
    public static void main(String[] args) throws InterruptedException {
        SynchronizedCounter example = new SynchronizedCounter();
        
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment();
                }
            });
            threads[i].start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        System.out.println("Counter: " + example.getCounter());
        // Выведет: Counter: 10000 ✓
    }
}

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

Поток A                    counter                    Поток B
[LOCK] 
      Read(5) ─────────────────────────────────────>
Compute(6)                                        ожидает...
Write(6)  
[UNLOCK]
                         (value = 6)
                                                   [LOCK]
                                                   Read(6)
                                                   Compute(7)
                                                   Write(7)
                                                   [UNLOCK]
                         (value = 7) ✓

Только один поток может держать lock одновременно.

Решение 2: AtomicInteger

Лучший вариант для простых операций — использовать AtomicInteger из пакета java.util.concurrent.atomic:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerCounter {
    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 {
        AtomicIntegerCounter example = new AtomicIntegerCounter();
        
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    example.increment();
                }
            });
            threads[i].start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        System.out.println("Counter: " + example.getCounter());
        // Выведет: Counter: 10000 ✓
    }
}

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

Он использует Compare-And-Swap (CAS) алгоритм без блокировок (lock-free):

public class AtomicIntegerSimplified {
    private volatile int value;
    
    public void incrementAndGet() {
        while (true) {
            int current = value;
            int next = current + 1;
            
            // Атомарно: если value == current, то устанавливаем next
            // Если установили успешно — выход
            // Если нет — повтор
            if (compareAndSet(current, next)) {
                break;
            }
        }
    }
    
    private synchronized boolean compareAndSet(int expect, int update) {
        if (value == expect) {
            value = update;
            return true;
        }
        return false;
    }
}

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

ПодходПотокобезопасностьПроизводительностьУдобство
int counter++❌ НЕТБыстроПростой синтаксис
synchronized✓ ДАМедленно (блокировка)Простой синтаксис
AtomicInteger✓ ДАБыстро (CAS)Нужны методы incrementAndGet()
volatileЧастичноБыстроВидимость, но не атомарность

Атомарные операции в AtomicInteger

AtomicInteger atomic = new AtomicInteger(5);

atomic.incrementAndGet();        // 6 (атомарно)
atomic.decrementAndGet();        // 5 (атомарно)
atomic.addAndGet(3);             // 8 (атомарно)
atomic.getAndSet(10);            // возвращает 8, устанавливает 10
atomic.compareAndSet(10, 20);    // если == 10, то установить 20

Другие атомарные типы

import java.util.concurrent.atomic.*;

AtomicBoolean bool = new AtomicBoolean(false);
AtomicLong longVal = new AtomicLong(0);
AtomicReference<String> ref = new AtomicReference<>("initial");
AtomicIntegerArray array = new AtomicIntegerArray(10);

Правило большого пальца

  • Если переменная используется только в одном потоке — используй обычный int
  • Если переменная используется в нескольких потоках — используй AtomicInteger
  • Если нужны сложные операции (несколько переменных) — используй synchronized

Вывод

Инкремент (i++) НЕ является атомарной операцией. В многопоточной среде это приводит к race conditions и потере данных. Для обеспечения потокобезопасности:

  1. Используйте AtomicInteger (рекомендуется для счётчиков)
  2. Используйте synchronized (для сложных критических секций)
  3. Избегайте обычного ++ в многопоточном коде

Это один из самых частых источников ошибок в многопоточных Java-приложениях.