1. Чому i++ не працює в багатопоточності?
Почнемо з класичної задачі: маємо змінну‑лічильник, наприклад, кількість оброблених заявок або число завантажених файлів. Ми хочемо, щоб кілька потоків збільшували цей лічильник. Що може піти не так, якщо просто написати i++?
Приклад: гонка даних на інкременті
public class Counter {
public int count = 0;
public void increment() {
count++; // Неатомарно!
}
}
Уявімо, що два потоки одночасно викликають increment(). Обидва потоки читають старе значення, обидва збільшують його на 1 і обидва записують… одне й те саме нове значення! У результаті один інкремент «втрачається». Якщо повторювати це багато разів, підсумкове значення буде меншим за очікуване.
Чому так відбувається?
Операція i++ насправді складається з трьох кроків:
- Прочитати значення змінної (наприклад, 5).
- Збільшити це значення на 1.
- Записати нове значення назад у пам’ять.
А в багатопоточному середовищі інший потік може встигнути змінити змінну між цими кроками. Підсумок — «гонка даних» (race condition).
Що таке атомарні операції?
Атомарна операція — це дія, яка або виконується повністю, або не виконується зовсім, і жоден інший потік не може «втрутитися» всередину цієї операції.
У Java є набір класів, що надають такі операції для примітивів і посилань. Вони розташовані в пакеті java.util.concurrent.atomic. Найпопулярніші:
- AtomicInteger — атомарний цілочисельний тип.
- AtomicLong — атомарний long.
- AtomicBoolean — атомарний boolean.
- AtomicReference<T> — атомарне посилання на об’єкт будь‑якого типу.
2. AtomicInteger: потокобезпечний лічильник
Оголошення та базове використання
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Атомарне збільшення
}
public int get() {
return count.get();
}
}
Тут incrementAndGet() виконує «збільшити й повернути нове значення» як одну неподільну операцію. Навіть якщо 100 потоків одночасно викличуть цей метод, жоден інкремент не загубиться.
Корисні методи:
| Метод | Опис |
|---|---|
|
Отримати поточне значення |
|
Встановити значення |
|
Збільшити на 1 і повернути нове значення |
|
Повернути поточне значення й збільшити на 1 |
|
Збільшити на delta й повернути нове значення |
|
Якщо поточне значення дорівнює expect, встановити update (CAS) |
Приклад: багатопотоковий лічильник
Припустімо, маємо клас, який рахує кількість оброблених повідомлень у чаті.
public class MessageStatistics {
private final AtomicInteger messageCount = new AtomicInteger(0);
public void onMessageReceived() {
int newCount = messageCount.incrementAndGet();
System.out.println("Усього повідомлень: " + newCount);
}
public int getMessageCount() {
return messageCount.get();
}
}
Внутрішня робота: як працює AtomicInteger?
Всередині AtomicInteger використовує спеціальну інструкцію процесора — CAS (Compare-And-Swap, «порівняти‑й‑підмінити»). Це атомарна операція, що порівнює поточне значення змінної з очікуваним і, якщо вони збігаються, записує нове значення. Якщо інший потік встиг змінити змінну — операція не виконується, і спроба повторюється.
Схема роботи:
1. Читаємо поточне значення (наприклад, 5)
2. Порівнюємо з очікуваним (5)
3. Якщо збігається — записуємо нове значення (6)
4. Якщо не збігається — повторюємо спробу
Усе це відбувається дуже швидко й без блокувань (lock‑free). Тому атомарні класи часто швидші, ніж synchronized, особливо за великої кількості потоків.
3. AtomicReference: атомарне посилання на об’єкт
AtomicReference<T> — це універсальний атомарний контейнер для будь‑якого об’єкта. Він дає змогу безпечно змінювати посилання на об’єкт із різних потоків.
Приклад: потокобезпечне оновлення посилання
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceExample {
private final AtomicReference<String> latestMessage = new AtomicReference<>("");
public void updateMessage(String message) {
latestMessage.set(message);
}
public String getLatestMessage() {
return latestMessage.get();
}
}
Застосування compareAndSet
Найцікавіша операція — compareAndSet(expected, newValue). Вона дає змогу оновити значення лише якщо воно не змінилося з моменту останнього читання.
public void safeUpdate(String oldValue, String newValue) {
boolean success = latestMessage.compareAndSet(oldValue, newValue);
if (success) {
System.out.println("Оновлення виконано успішно!");
} else {
System.out.println("Хтось уже змінив значення, спробуйте ще раз.");
}
}
Це основа неблокувальних алгоритмів: від черг і стеків до кешів, де важливо уникати зайвих блокувань.
4. Приклади використання у застосунку
Приклад 1: багатопотоковий лічильник повідомлень
public class ChatRoom {
private final AtomicInteger messageCount = new AtomicInteger(0);
public void receiveMessage(String message) {
// ... обробка повідомлення ...
int count = messageCount.incrementAndGet();
System.out.println("Нове повідомлення: " + message + ". Усього повідомлень: " + count);
}
}
Приклад 2: безпечне оновлення посилання на останнє повідомлення
public class ChatRoom {
private final AtomicReference<String> lastMessage = new AtomicReference<>("");
public void receiveMessage(String message) {
lastMessage.set(message);
// ... обробка ...
}
public String getLastMessage() {
return lastMessage.get();
}
}
Якщо потрібно оновлювати посилання лише коли останнє повідомлення не змінилося (щоб уникнути «втрати» під час одночасних оновлень), використовуйте compareAndSet.
5. Обмеження та підводні камені
Коли атомарні класи — не панацея?
Атомарні змінні чудово підходять для простих операцій: інкремент, встановлення значення, перевірка й заміна. Але якщо потрібно оновити кілька змінних одночасно, атомарність уже не гарантується. Наприклад, якщо у вас два лічильники і ви хочете збільшити їх обидва як одну операцію — тут потрібен synchronized або інший механізм синхронізації.
Приклад неправильного використання
// Неатомарно!
if (ref.get() == null) {
ref.set("Hello");
}
Між get() і set(...) інший потік може змінити значення, і умова вже буде хибною. Для таких випадків використовуйте compareAndSet.
Атомарні класи ≠ потокобезпечні об’єкти
Якщо об’єкт, на який вказує AtomicReference, сам не є потокобезпечним, то заміна посилання буде атомарною, але зміна полів об’єкта — ні. Наприклад, якщо ви зберігаєте в AtomicReference<List<String>> звичайний ArrayList, то сам список не стане потокобезпечним.
6. Розширені атомарні класи
У пакеті java.util.concurrent.atomic є й інші корисні класи:
- AtomicLong, AtomicBoolean — для long і boolean.
- AtomicIntegerArray, AtomicReferenceArray — атомарні операції над масивами.
- LongAdder, LongAccumulator — для високонавантажених лічильників.
LongAdder і LongAccumulator
Якщо у вас дуже багато потоків і звичайний AtomicInteger стає «вузьким місцем» (усі потоки змагаються за одну змінну), використовуйте LongAdder. Він розбиває лічильник на кілька внутрішніх комірок і підсумовує їх під час запиту значення, що дає виграш за високої конкуренції.
import java.util.concurrent.atomic.LongAdder;
public class FastCounter {
private final LongAdder adder = new LongAdder();
public void increment() {
adder.increment();
}
public long getCount() {
return adder.sum();
}
}
7. Типові помилки під час роботи з атомарними змінними
Помилка № 1: Очікування атомарності складних операцій.
Якщо потрібно виконати кілька дій над значенням, атомарні класи не врятують — між кроками інший потік може змінити дані. Для складених операцій використовуйте compareAndSet або синхронізацію.
Помилка № 2: Ігнорування потокобезпечності вкладених об’єктів.
Якщо в AtomicReference лежить звичайний об’єкт, його методи й поля не стають потокобезпечними. Атомарною є лише заміна посилання.
Помилка № 3: Використання атомарних класів без потреби.
В однопотоковому коді атомарні типи зайві й трохи повільніші за звичайні змінні через додаткові перевірки.
Помилка № 4: Передчасна оптимізація.
Іноді простіше й надійніше використовувати synchronized, особливо коли логіка складна й зачіпає кілька змінних одночасно. Не завжди виправдано будувати lock‑free рішення.
Помилка № 5: Забули про проблему ABA.
Рідкісний, але важливий випадок: значення змінюється з A на B і знову на A — compareAndSet «вважає», що нічого не змінювалося. Для таких сценаріїв використовуйте спецкласи на кшталт AtomicStampedReference (або AtomicMarkableReference).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ