Для создания надёжных многопоточных приложений важно правильно и эффективно управлять общими данными. В Java для этого часто применяют volatile-переменные и атомарные классы из пакета java.util.concurrent.atomic. Оба механизма обеспечивают взаимодействие потоков с общим состоянием, но решают разные задачи.
Зачем нужны атомарные классы?
Атомарные классы позволяют выполнять над одной переменной неделимые операции без явной синхронизации. Инкремент, декремент, сложение или compare-and-set (CAS) завершаются как один логический шаг: другой поток не может увидеть промежуточное состояние и вмешаться между чтением и записью.
Пакет java.util.concurrent.atomic содержит, в частности:
AtomicInteger;AtomicLong;AtomicBoolean;AtomicReference.
Эти классы предоставляют готовые потокобезопасные методы для чтения, изменения и условного обновления значений. В простых сценариях они позволяют обойтись без synchronized и явных блокировок.
Основные свойства атомарных классов
- Атомарность. Операция неделима: другие потоки видят только состояние до неё или после неё.
- Неблокирующее выполнение. Большинство методов опирается на низкоуровневые инструкции процессора и не приостанавливает поток в ожидании монитора.
- Lock-free-подход. Операции обычно не используют взаимное исключение, поэтому снижается риск длительной конкуренции и взаимной блокировки.
- Видимость. Чтения и записи имеют необходимые гарантии Java Memory Model, поэтому результат обновления доступен другим потокам.
Это не означает, что atomics всегда быстрее блокировок: при высокой конкуренции повторные CAS-попытки тоже могут быть дорогими. Выбор зависит от структуры алгоритма и нагрузки.
Пример с AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // атомарный инкремент
}
public int getValue() {
return counter.get();
}
public static void main(String[] args) {
AtomicExample example = new AtomicExample();
// Создаём 1000 потоков, увеличивающих счётчик
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(example::increment);
threads[i].start();
}
// Ждём завершения всех потоков
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Final counter value: " + example.getValue());
}
}
incrementAndGet() выполняет чтение, увеличение и запись как единую атомарную операцию. Поэтому после завершения всех 1000 потоков счётчик содержит ожидаемое значение. Обычный int, даже объявленный как volatile, мог бы потерять обновления при одновременном выполнении ++.
Различия между volatile и атомарными классами
- Видимость и атомарность.
volatileгарантирует, что потоки видят последние записи, но не защищает составные действия.- Атомарные классы обеспечивают и видимость, и атомарность предоставляемых операций.
- Сложные обновления.
volatileподходит для независимых чтений и присваиваний.- Atomics предлагают
incrementAndGet(),decrementAndGet(),addAndGet()иcompareAndSet()для безопасного read-modify-write.
- Реализация.
volatileиспользует правила Java Memory Model для публикации значения и упорядочивания доступа.- Атомарные классы обычно применяют аппаратный CAS и при необходимости повторяют операцию.
- Сценарии использования.
volatileвыбирают для флагов, признаков готовности и простого состояния.- Atomics подходят для счётчиков, ссылок и значений, которые часто изменяют несколько потоков.
Итоги
Понимание различий между volatile и атомарными классами помогает выбрать минимально необходимый механизм синхронизации. volatile решает проблему видимости, а атомарные классы дополнительно обеспечивают неделимые составные операции. Когда инвариант охватывает одну переменную, atomics часто дают простой неблокирующий вариант. Если же нужно согласованно изменять несколько значений, выполнять длинную критическую секцию или ждать условия, лучше использовать блокировки или другие высокоуровневые средства конкурентности.