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

Как работать с примитивами в многопоточности

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

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

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

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

Как работать с примитивами в многопоточности

Работас примитивами в многопоточной среде — это одна из самых сложных и опасных задач в Java. Примитивы обычно хранятся в стеке и не являются потокобезопасными, поэтому нужны специальные техники и классы.

1. Проблема: Visibility и Race Condition

public class Counter {
  private int count = 0; // НЕ потокобезопасно!
  
  public void increment() {
    count++; // Это не атомная операция!
    // На самом деле это:
    // 1. Прочитать count
    // 2. Увеличить на 1
    // 3. Записать обратно
  }
  
  public int getCount() {
    return count; // Могу видеть устаревшее значение
  }
}

// Результат: потеря обновлений при параллельном доступе
// Thread1 читает: 0, увеличивает на 1, пишет: 1
// Thread2 читает: 0, увеличивает на 1, пишет: 1
// Ожидали 2, получили 1

2. Решение 1: volatile для visibility

public class Counter {
  private volatile int count = 0;
  
  public void increment() {
    count++; // Всё ещё НЕ атомная операция!
  }
  
  public int getCount() {
    return count; // Гарантия видеть последнее значение
  }
}

// volatile гарантирует:
// 1. Видимость изменений в других потоках
// 2. Запрещает оптимизации компилятором
// НО: не гарантирует атомарность count++

3. Решение 2: synchronized для атомарности

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

// synchronized гарантирует:
// 1. Только один поток может быть в методе
// 2. Visibility всех переменных
// НО: может быть медленно из-за lock contention

4. Решение 3: AtomicInteger (рекомендуется)

Это наилучший способ для примитивов:

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();
  }
}

// Методы AtomicInteger:
count.get();              // Прочитать
count.set(5);             // Установить
count.incrementAndGet();   // ++count, вернуть новое значение
count.getAndIncrement();   // count++, вернуть старое значение
count.addAndGet(10);       // count += 10
count.getAndAdd(10);       // count += 10, вернуть старое
count.compareAndSet(0, 1); // Атомная CAS операция

5. Другие Atomic классы для примитивов

import java.util.concurrent.atomic.*;

// Для long значений
AtomicLong duration = new AtomicLong(0);
duration.getAndAdd(100);

// Для boolean
AtomicBoolean isRunning = new AtomicBoolean(true);
isRunning.set(false);
if (isRunning.compareAndSet(true, false)) {
  // Операция выполнена только если было true
}

// Для любого объекта/примитива обёрнутого в объект
AtomicReference<Integer> ref = new AtomicReference<>(0);
ref.getAndSet(5);

6. CAS операции (Compare-And-Swap)

Это основа для безблокировки (lock-free) программирования:

AtomicInteger value = new AtomicInteger(10);

// CAS: если текущее значение 10, установить 20, вернуть true
boolean success = value.compareAndSet(10, 20);
if (success) {
  System.out.println("Успешно обновили с 10 на 20");
}

// Практический пример: retry loop
public void updateValue(int delta) {
  while (true) {
    int oldValue = value.get();
    int newValue = oldValue + delta;
    if (value.compareAndSet(oldValue, newValue)) {
      break; // Успех
    }
    // Иначе retry
  }
}

7. Проблемы с примитивами: ABA Problem

AtomicInteger counter = new AtomicInteger(10);

// Thread1
int oldValue = counter.get(); // Читает 10
// Контекст переключился на Thread2

// Thread2
counter.set(20);
counter.set(10);
// Контекст вернулся на Thread1

// Thread1
if (counter.compareAndSet(oldValue, 15)) {
  // ОПАСНО! Значение "выглядит" как было 10, но были изменения
}

// Решение: использовать AtomicStampedReference
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(10, 0);
int[] stampHolder = new int[1];
int value = ref.get(stampHolder);
int stamp = stampHolder[0];
ref.compareAndSet(value, 15, stamp, stamp + 1);

8. Практические примеры

Счётчик параллельных операций:

public class ThreadSafeCounter {
  private final AtomicInteger count = new AtomicInteger(0);
  private final AtomicInteger activeThreads = new AtomicInteger(0);
  
  public void startTask() {
    activeThreads.incrementAndGet();
  }
  
  public void completeTask() {
    count.incrementAndGet();
    activeThreads.decrementAndGet();
  }
  
  public int getTotalCompleted() {
    return count.get();
  }
  
  public int getActiveCount() {
    return activeThreads.get();
  }
}

Флаг для управления потоками:

public class Service {
  private final AtomicBoolean running = new AtomicBoolean(false);
  
  public boolean start() {
    return running.compareAndSet(false, true);
  }
  
  public void stop() {
    running.set(false);
  }
  
  public void doWork() {
    if (running.get()) {
      // Выполнить работу
    }
  }
}

Счётчик с максимумом:

public class LimitedCounter {
  private final AtomicInteger count = new AtomicInteger(0);
  private final int limit;
  
  public LimitedCounter(int limit) {
    this.limit = limit;
  }
  
  public boolean tryIncrement() {
    while (true) {
      int oldValue = count.get();
      if (oldValue >= limit) {
        return false; // Лимит достигнут
      }
      if (count.compareAndSet(oldValue, oldValue + 1)) {
        return true;
      }
    }
  }
}

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

// 1. volatile + synchronized (медленно, но просто)
private volatile int count = 0;
public synchronized void increment() { count++; }

// 2. synchronized в целом (медленнее, чем atomic)
private int count = 0;
public synchronized void increment() { count++; }

// 3. AtomicInteger (быстро и просто) ✅
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }

10. Когда NOT использовать Atomic

// Если нужна синхронизация нескольких переменных
public class Account {
  private final AtomicLong balance = new AtomicLong(0);
  private final AtomicLong lastUpdate = new AtomicLong(0);
  
  // ПРОБЛЕМА: balance и lastUpdate обновляются не атомно!
  public void deposit(long amount) {
    balance.addAndGet(amount);
    lastUpdate.set(System.currentTimeMillis());
    // Между этими операциями другой поток может вмешаться
  }
  
  // Решение: synchronized для нескольких полей
  public synchronized void deposit(long amount) {
    balance += amount;
    lastUpdate = System.currentTimeMillis();
  }
}

В итоге: Для примитивов в многопоточности используй AtomicInteger/AtomicLong — это надёжно, быстро и выразительно. Для нескольких переменных — synchronized блоки. А volatile только для flags и simple visibility случаев.

Как работать с примитивами в многопоточности | PrepBro