Является ли реализация volatile long счетчика потокобезопасной, счетчик инкрементируется из разных потоков параллельно?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# 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!