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

Вернется ли 2000 при наличии двух потоков, увеличивающих volatile переменную равную 0

2.3 Middle🔥 121 комментариев
#Многопоточность

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

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

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

Вернется ли 2000 при наличии двух потоков, увеличивающих volatile переменную равную 0

Ответ: НЕТ, не вернется 2000. Это одна из самых частых ошибок в многопоточном программировании.

Почему не 2000?

Разберемся с примером:

public class VolatileIncrement {
    volatile int counter = 0;
    
    public void increment() {
        counter++;
    }
    
    public static void main(String[] args) throws InterruptedException {
        VolatileIncrement test = new VolatileIncrement();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                test.increment();
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                test.increment();
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println(counter);  // Вероятно НЕ 2000!
    }
}

Если запустить несколько раз, результат будет разным: 1500, 1800, 1950... но не обязательно 2000.

В чем проблема?

Ключевой момент: Volatile гарантирует видимость, НО НЕ атомарность!

Операция counter++ состоит из трех микрооперций:

  1. Read — прочитать текущее значение (допустим, 100)
  2. Increment — увеличить (100 + 1 = 101)
  3. Write — записать обратно (counter = 101)
Время →
Thread 1: [Read 100] → [+1] → [Write 101]
Thread 2:      [Read 100] → [+1] → [Write 101]

Результат: 101 (потеряли +1 от второго потока!)

Race Condition

// Синхронизация читать->изменять->писать невозможна без синхронизации
void increment() {
    // RACE CONDITION ТУТ!
    int temp = counter;      // Оба потока читают одно значение
    temp++;                  // Оба потока увеличивают
    counter = temp;          // Оба потока пишут одно и то же значение
}

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

volatile int counter = 0;  // Гарантирует:
  1. Visibility (видимость)

    • Изменения видны всем потокам сразу
    • Нет кэширования в регистрах процессора
  2. Ordering (упорядочивание)

    • Операции с volatile полями не переупорядочиваются
    • Как забор памяти (memory fence)

НО НЕ гарантирует:

  • Атомарность составных операций (read-modify-write)
  • Безопасность конкурентного доступа

Демонстрация проблемы

public class VolatileVsProblem {
    volatile int count = 0;
    
    public void testRaceCondition() throws InterruptedException {
        // Попробуем 100 раз
        int results[] = new int[100];
        
        for (int run = 0; run < 100; run++) {
            count = 0;
            
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) count++;
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) count++;
            });
            
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            
            results[run] = count;
        }
        
        // Проверяем результаты
        for (int i = 0; i < 100; i++) {
            if (results[i] != 2000) {
                System.out.println("Run " + i + ": " + results[i]);
            }
        }
    }
}

Вывод: большинство результатов будут < 2000.

Решение 1: synchronized блок

public class SynchronizedIncrement {
    int counter = 0;  // НЕ нужен volatile
    
    public synchronized void increment() {
        counter++;
    }
    
    // Результат: ВСЕГДА 2000
}

Почему работает:

  • synchronized обеспечивает взаимное исключение
  • Только один поток может выполнять критическую секцию
  • Атомарность гарантирована

Решение 2: AtomicInteger

public class AtomicIncrement {
    AtomicInteger counter = new AtomicInteger(0);
    
    public void increment() {
        counter.incrementAndGet();  // Атомарная операция
    }
    
    // Результат: ВСЕГДА 2000
}

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

  • Без блокировок (lock-free)
  • Использует CAS (Compare-And-Swap)
  • Выше производительность чем synchronized

Решение 3: ReentrantLock

public class LockIncrement {
    ReentrantLock lock = new ReentrantLock();
    int counter = 0;
    
    public void increment() {
        lock.lock();
        try {
            counter++;
        } finally {
            lock.unlock();
        }
    }
    
    // Результат: ВСЕГДА 2000
}

Сравнение подходов

ПодходРезультатАтомарностьПроизводительность
volatileНЕ 2000НетОчень высокая
synchronized2000ДаСредняя
AtomicInteger2000ДаВысокая (lock-free)
ReentrantLock2000ДаВысокая

Еще один пример: видимость vs атомарность

volatile int x = 0;

Thread 1: x = 1;      // Видно Thread 2
Thread 2: x = 2;      // Видно Thread 1
Thread 3: System.out.println(x);  // Вернет 1 или 2

// Поток 3 ВИДИТ изменения, но нет гарантии порядка

Правило памяти (Memory Visibility)

Volatile создает "забор памяти" (memory fence):

Before volatile write      After volatile write
┌─────────────┐            ┌─────────────┐
│ L1 Cache    │  FLUSH →  │ Main Memory │
│ L2 Cache    │  ────→    │             │
│ Registers   │           │             │
└─────────────┘           └─────────────┘

Before volatile read      After volatile read
┌─────────────┐            ┌─────────────┐
│ L1 Cache    │  REFRESH ← │ Main Memory │
│ L2 Cache    │  ←──────   │             │
│ Registers   │           │             │
└─────────────┘           └─────────────┘

Но это не защищает от:

Thread 1:
read x (value = 5)
increment (5 + 1 = 6)      ← ПРОБЛЕМА: другой поток может вмешаться
write x (x = 6)

Вывод

Для счетчика, увеличиваемого несколькими потоками:

  1. ✅ Используй AtomicInteger (лучший выбор)
  2. ✅ Используй synchronized (если просто)
  3. ✅ Используй ReentrantLock (если нужна гибкость)
  4. ❌ НЕ используй volatile одиночное — это даст непредсказуемый результат

Памятка: Volatile защищает от проблем видимости, но НЕ от race condition при составных операциях. Для атомарности нужны synchronized, atomics или locks.