← Назад к вопросам
Какая проблема возникнет при использовании volatile счётчика, если несколько потоков параллельно увеличивают его значение?
2.0 Middle🔥 111 комментариев
#Многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Какая проблема возникнет при использовании volatile счётчика, если несколько потоков параллельно увеличивают его значение?
Это классическая проблема в многопоточном Java. volatile не гарантирует атомарность операций, и результат будет неправильный. Подробно разбираю проблему и решения.
Чего ожидает начинающий разработчик
public class VolatileCounterProblem {
private volatile int counter = 0; // Кажется, что это достаточно!
public void incrementCounter() {
counter++; // Проблема здесь!
}
public static void main(String[] args) throws InterruptedException {
VolatileCounterProblem app = new VolatileCounterProblem();
// 10 потоков, каждый увеличивает счётчик на 1000
// Ожидаем результат: 10000
// Получим: случайное число < 10000 (например, 9856)
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
app.incrementCounter();
}
}).start();
}
Thread.sleep(1000);
System.out.println("Counter: " + app.counter); // Неправильный результат!
}
}
Почему volatile недостаточно?
volatile гарантирует только:
- Видимость (visibility) — все потоки видят последнее значение
- НЕ гарантирует атомарность (atomicity)
// counter++ это ТРИ операции:
// 1. Прочитать текущее значение: temp = counter (READ)
// 2. Увеличить: temp = temp + 1 (COMPUTE)
// 3. Записать обратно: counter = temp (WRITE)
// Временная шкала:
// Thread 1 (T1): READ(counter=0) → COMPUTE(temp=1) → WRITE(counter=1)
// Thread 2 (T2): READ(counter=0) → COMPUTE(temp=1) → WRITE(counter=1)
//
// Результат: counter=1 (вместо ожидаемого 2!)
// Операция T2 затёрла результат T1
Визуализация проблемы
Время ▼
T1 T2 T3
| | |
READ(0) | |
| READ(0) |
| | READ(0)
| | |
COMPUTE(0+1) COMPUTE(0+1) COMPUTE(0+1)
| | |
| WRITE(1) |
| | |
WRITE(1) | |
| | WRITE(1)
| | |
Результат: counter = 1
Ожидание: counter = 3
Потеря данных: 2 увеличения потеряны!
Проблема на практике
public class VolatileVsAtomic {
private volatile int volatileCounter = 0;
private AtomicInteger atomicCounter = new AtomicInteger(0);
public void testPerformance() throws InterruptedException {
// Test 1: volatile счётчик
long startTime = System.currentTimeMillis();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
volatileCounter++; // Race condition!
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long volatileTime = System.currentTimeMillis() - startTime;
System.out.println("Volatile counter: " + volatileCounter + " (ожидаем 1000000)");
System.out.println("Время: " + volatileTime + "ms");
// Test 2: AtomicInteger
startTime = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
atomicCounter.incrementAndGet(); // Атомарно!
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long atomicTime = System.currentTimeMillis() - startTime;
System.out.println("\nAtomic counter: " + atomicCounter.get()); // Всегда 1000000
System.out.println("Время: " + atomicTime + "ms");
}
}
// Output примерный:
// Volatile counter: 897654 (ожидаем 1000000) ← НЕПРАВИЛЬНО!
// Время: 150ms
//
// Atomic counter: 1000000 ← ПРАВИЛЬНО!
// Время: 450ms (медленнее, но правильно)
Решение 1: synchronized
public class SynchronizedCounter {
private int counter = 0;
// Синхронизируем метод
public synchronized void increment() {
counter++; // Теперь атомарно!
}
public synchronized int getCounter() {
return counter;
}
}
// Или с блоком:
public class SynchronizedBlockCounter {
private int counter = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
counter++; // Атомарно
}
}
public int getCounter() {
synchronized (lock) {
return counter;
}
}
}
Решение 2: AtomicInteger (рекомендуется)
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
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 {
AtomicCounter app = new AtomicCounter();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
app.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Counter: " + app.getCounter()); // 10000 ✓
}
}
Решение 3: Lock (более гибко)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
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();
}
}
// С timeout'ом
public boolean incrementWithTimeout() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
counter++;
return true;
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
}
Решение 4: LongAdder (для high-contention сценариев)
import java.util.concurrent.atomic.LongAdder;
public class LongAdderCounter {
private LongAdder counter = new LongAdder();
public void increment() {
counter.increment(); // Очень быстро при высокой контенции!
}
public long getCounter() {
return counter.sum();
}
public static void main(String[] args) throws InterruptedException {
LongAdderCounter app = new LongAdderCounter();
long startTime = System.currentTimeMillis();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000000; j++) {
app.increment();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("Counter: " + app.getCounter()); // 10000000
System.out.println("Time: " + (System.currentTimeMillis() - startTime) + "ms");
// LongAdder ~100ms (почти как volatile без потери данных!)
// AtomicInteger ~500ms
}
}
Сравнение подходов
Подход | Атомарность | Производительность | Простота | Когда использовать
─────────────────────────────────────────────────────────────────────────────────────
volatile | НЕТ ❌ | Очень быстро | Простой | Только для flag'ов, НЕ для счётчиков
synchronized | ДА ✓ | Медленно | Средний | Не высоконагруженные счётчики
AtomicInteger | ДА ✓ | Быстро | Средний | Стандартный выбор
LongAdder | ДА ✓ | Очень быстро | Средний | High-contention счётчики (100+ потоков)
ReentrantLock | ДА ✓ | Быстро | Сложный | Когда нужна гибкость (tryLock, timeout)
Best Practices
// ❌ ПЛОХО: volatile для счётчика
private volatile int counter = 0;
public void increment() { counter++; }
// ✅ ХОРОШО: AtomicInteger
private AtomicInteger counter = new AtomicInteger(0);
public void increment() { counter.incrementAndGet(); }
// ✅ ХОРОШО: Для высоконагруженных систем
private LongAdder counter = new LongAdder();
public void increment() { counter.increment(); }
// ✅ ХОРОШО: Если нужна гибкость
private final Lock lock = new ReentrantLock();
private int counter = 0;
public void increment() {
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
}
Когда volatile работает
// ✅ ПРАВИЛЬНО: volatile для boolean флага (one-way operation)
private volatile boolean shutdown = false;
public void shutdownRequested() {
shutdown = true; // Один поток пишет
}
public boolean isShutdown() {
return shutdown; // Другие потоки читают
}
// ✅ ПРАВИЛЬНО: volatile для reference'ов
private volatile CachedData cache; // Один поток пишет, другие читают
Итоговые выводы
- volatile НЕ гарантирует атомарность — это частая ошибка
- counter++ это три операции, не одна
- Race condition приводит к потере данных
- Используй AtomicInteger или LongAdder для счётчиков
- synchronized подходит для простых случаев, но медленнее
- LongAdder быстрее при high-contention
- Всегда проверяй многопоточный код под нагрузкой
В production коде я всегда использую AtomicInteger для счётчиков и LongAdder для высоконагруженных систем с сотнями потоков, обновляющих один счётчик.