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

Является ли реализация volatile long счетчика потокобезопасной, счетчик инкрементируется из разных потоков параллельно?

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

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

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

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

# Volatile long счетчик: потокобезопасность

Короткий ответ

НЕТ, volatile long счетчик НЕ потокобезопасен при инкременте из разных потоков!

Это частая ошибка в собеседованиях. Volatile гарантирует только видимость изменений между потоками, но НЕ гарантирует атомность операции.

Почему не потокобезопасен

Проблема: инкремент не атомарен

Операция counter++ состоит из трёх шагов:

1. READ:    получить текущее значение из памяти (например, 5)
2. MODIFY:  увеличить на 1 (5 + 1 = 6)
3. WRITE:   записать новое значение в память

Это не атомарная операция! Между шагами могут вмешаться другие потоки:

Поток 1:                    Поток 2:              Значение в памяти

READ counter (5)            
                            READ counter (5)      counter = 5
MODIFY (5+1=6)              
                            MODIFY (5+1=6)       
WRITE counter = 6           
                            WRITE counter = 6     counter = 6
                                                  ❌ Потеря одного инкремента!

Ожидаемо: counter = 7 (два инкремента) На самом деле: counter = 6 (потеря одного инкремента)

Практический пример

public class VolatileCounterTest {
    private volatile long counter = 0;  // ❌ ПЛОХО!
    
    public void increment() {
        counter++;  // НЕ АТОМАРНАЯ ОПЕРАЦИЯ!
    }
    
    public static void main(String[] args) throws InterruptedException {
        VolatileCounterTest test = new VolatileCounterTest();
        
        // Запускаем 10 потоков
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                // Каждый поток делает 100000 инкрементов
                for (int j = 0; j < 100000; j++) {
                    test.increment();
                }
            });
            threads[i].start();
        }
        
        // Ждём, пока все потоки закончат
        for (Thread thread : threads) {
            thread.join();
        }
        
        // Ожидаем: 10 * 100000 = 1000000
        System.out.println("Expected: 1000000");
        System.out.println("Actual: " + test.counter);
        // Результат: 987342 (или другое число < 1000000)
        // ❌ Потеря инкрементов!
    }
}

При запуске получишь результат типа 987342 вместо ожидаемого 1000000. Это race condition!

Что гарантирует volatile

Volatile гарантирует видимость:

public class VolatileVisibility {
    private volatile boolean flag = false;
    private int value = 0;
    
    // Поток 1
    public void write() {
        value = 42;
        flag = true;  // ✅ volatile гарантирует, что Поток 2 увидит это
    }
    
    // Поток 2
    public void read() {
        if (flag) {  // ✅ volatile гарантирует видимость
            System.out.println(value);  // Выведет 42, а не 0
        }
    }
}

Но volatile НЕ гарантирует атомность:

private volatile long counter = 0;
public void increment() {
    counter++;  // ❌ Не атомарна, несмотря на volatile!
}

Правильные решения

1. AtomicLong — РЕКОМЕНДУЕТСЯ

import java.util.concurrent.atomic.AtomicLong;

public class AtomicCounterTest {
    private AtomicLong counter = new AtomicLong(0);  // ✅ ПРАВИЛЬНО!
    
    public void increment() {
        counter.incrementAndGet();  // ✅ Атомарная операция
    }
    
    public static void main(String[] args) throws InterruptedException {
        AtomicCounterTest test = new AtomicCounterTest();
        
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 100000; j++) {
                    test.increment();
                }
            });
            threads[i].start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        System.out.println("Result: " + test.counter.get());  // ✅ 1000000
    }
}

Преимущества:

  • Атомарна
  • Без блокировок (lock-free)
  • Высокая производительность
  • Специально разработана для счётчиков

2. synchronized — когда нужна блокировка

public class SynchronizedCounter {
    private long counter = 0;  // Не нужен volatile
    
    public synchronized void increment() {  // ✅ Блокировка
        counter++;
    }
    
    public synchronized long get() {
        return counter;
    }
}

Недостатки:

  • Медленнее AtomicLong (блокировка дорогая)
  • Может быть deadlock

3. ReentrantReadWriteLock — если много читов, мало写

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockCounter {
    private long counter = 0;
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void increment() {
        lock.writeLock().lock();
        try {
            counter++;
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public long get() {
        lock.readLock().lock();
        try {
            return counter;
        } finally {
            lock.readLock().unlock();
        }
    }
}

Сравнение методов

Метод              Безопас  Блокиров  Произво   Простота
─────────────────────────────────────────────────────────
volatile            ❌       ❌       Макс.    Макс.
AtomicLong         ✅       ❌       Высок    Высок
synchronized       ✅       ✅       Средняя  Средняя
ReentrantLock      ✅       ✅       Низкая   Низкая

Что произойдёт в реальности

private volatile long counter = 0;

// Компилятор генерирует что-то типа:
// READ:   Load counter from memory
// MODIFY: Inc (add 1)
// WRITE:  Store counter to memory

// Volatile только гарантирует:
// - Правильный порядок чтения/записи
// - Видимость между потоками
// - НЕ гарантирует: что между READ и WRITE другой поток не выполнит свой READ-MODIFY-WRITE

На собеседовании

Ответь так:

"Нет, volatile long counter++ НЕ потокобезопасен при параллельном инкременте из разных потоков.

Почему: Операция инкремента состоит из трёх шагов (READ-MODIFY-WRITE), и это НЕ атомарная операция. Volatile гарантирует только видимость, но НЕ гарантирует атомность.

Пример race condition: Если два потока одновременно прочитают значение 5, оба увеличат его на 1 и запишут 6. Вместо ожидаемого 7, получим 6.

Решение: Используй AtomicLong.incrementAndGet() — это атомарная lock-free операция."

Выводы

  • volatile long counter++ — НЕ потокобезопасна
  • AtomicLong.incrementAndGet() — потокобезопасна и быстра
  • synchronized void increment() — потокобезопасна, но медленнее
  • 📚 Volatile = видимость, НЕ атомность
  • 🔧 Для счётчиков — только AtomicLong!