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

Как работает Volatile?

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

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

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

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

Как работает Volatile

Volatile - это модификатор в Java, который гарантирует видимость изменений переменной между потоками. Это один из самых важных, но неправильно понимаемых механизмов многопоточности.

Проблема без Volatile

public class VolatileProblem {
    private boolean ready = false; // Без volatile
    private int value = 0;
    
    // Поток 1
    public void writer() {
        value = 42;
        ready = true; // Сигнал что значение готово
    }
    
    // Поток 2
    public void reader() {
        while (!ready) {
            // МОЖЕТ крутиться вечно!
            // Хотя Поток 1 уже установил ready = true
        }
        System.out.println(value); // Может вывести 0 или 42
    }
}

Почему это происходит?

Без volatile каждый поток может кэшировать значение переменной в своём локальном кэше (процессорном регистре). Изменения в одном потоке не видны другому потоку.

Поток 1 (CPU1)              Память              Поток 2 (CPU2)
┌──────────────┐           ┌─────────┐         ┌──────────────┐
│ ready=false  │    ────>  │ ready=0 │  <────  │ ready=false  │
│ (в L1 cache) │           └─────────┘         │ (в L1 cache) │
└──────────────┘                               └──────────────┘
    ↓
Устанавливает ready=true в своём cache
Это значение может не синхронизироваться в shared memory
Поток 2 продолжает видеть в своём cache ready=false

Решение: Volatile

public class VolatileSolution {
    private volatile boolean ready = false; // С volatile
    private volatile int value = 0;
    
    public void writer() {
        value = 42;
        ready = true; // Гарантированно запишется в shared memory
    }
    
    public void reader() {
        while (!ready) {
            // Каждая итерация будет читать из shared memory
        }
        System.out.println(value); // Всегда выведет 42
    }
}

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

1. Visibility (Видимость)

private volatile int counter = 0;

// Поток 1
counter = 100;
// Видимо другим потокам СРАЗУ

// Поток 2
int value = counter; // Гарантированно прочитает 100

Volatile гарантирует:

  • Что изменение попадёт в shared memory
  • Что чтение получит последнее значение из shared memory
  • Это создаёт "happens-before" отношение

2. Happens-Before Semantics

public class HappensBeforeExample {
    private volatile int flag = 0;
    private int x = 0, y = 0;
    
    // Поток 1
    public void threadOne() {
        x = 1;
        y = 1;
        flag = 1; // Volatile write
        // Гарантировано: (x=1, y=1) happens-before flag=1
    }
    
    // Поток 2
    public void threadTwo() {
        while (flag == 0); // Volatile read
        // Если мы прочитали flag=1, то x=1 и y=1 тоже видны
        System.out.println("x=" + x + ", y=" + y); // Всегда "x=1, y=1"
    }
}

Как это работает на уровне CPU

private volatile int value = 0;

public void write() {
    value = 42;
}

// Компилируется примерно в:
// MOV [shared_memory], RAX  <- запись с memory barrier
// MFENCE                     <- инструкция забора памяти

public int read() {
    return value;
}

// Компилируется примерно в:
// MFENCE                     <- инструкция забора памяти
// MOV RAX, [shared_memory]   <- чтение с memory barrier

Что Volatile НЕ гарантирует

private volatile int counter = 0;

// ❌ НЕБЕЗОПАСНО
public void increment() {
    counter++; // counter++ это 3 операции:
    // 1. Прочитать counter
    // 2. Увеличить на 1
    // 3. Записать обратно
    // Volatile гарантирует видимость, но НЕ атомарность!
}

// Scenario:
// Thread 1: читает counter=5 ────────┐
// Thread 2: читает counter=5 ────┐   │
// Thread 1: пишет counter=6      │   │
// Thread 2: пишет counter=6      │ <- Race condition!
// Результат: counter=6 вместо 7

// ✅ ПРАВИЛЬНО
public void incrementCorrect() {
    synchronized(this) {
        counter++; // Теперь атомарно
    }
}

// ИЛИ
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
    counter.incrementAndGet(); // Атомарно
}

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

1. Флаг завершения

public class ShutdownFlag {
    private volatile boolean shutdown = false;
    
    public void shutdown() {
        shutdown = true; // Все потоки увидят это немедленно
    }
    
    public void run() {
        while (!shutdown) {
            doWork();
        }
    }
}

2. Double-Checked Locking

public class Singleton {
    private volatile Singleton instance; // volatile ОБЯЗАТЕЛЕН!
    
    public Singleton getInstance() {
        if (instance == null) { // Первая проверка (быстро)
            synchronized(this) {
                if (instance == null) { // Вторая проверка (безопасно)
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    
    // Без volatile:
    // Поток 1 создаёт Singleton в конструкторе
    // Поток 2 видит instance != null, но не видит инициализацию полей
    // undefined behaviour!
}

3. Status флаги

public class ServerStatus {
    private volatile boolean running = false;
    private volatile long lastHeartbeat = System.currentTimeMillis();
    
    public void start() {
        running = true;
    }
    
    public void heartbeat() {
        lastHeartbeat = System.currentTimeMillis();
    }
    
    public void checkHealth() {
        long now = System.currentTimeMillis();
        if (running && (now - lastHeartbeat > 10000)) {
            System.out.println("Server is dead");
        }
    }
}

Поведение volatile с разными типами

// long и double требуют особого внимания
private volatile long largeNumber = 0;
private volatile double decimal = 0.0;

// На 32-битных системах long/double это 2 операции по 32 бита
// volatile гарантирует что оба слова будут записаны атомарно

// Обычный int/reference всегда атомарен
private volatile int normalInt = 0;
private volatile MyObject reference = null;

Performance considerations

// volatile имеет стоимость
private volatile int hotCounter = 0; // Часто читается

// Оптимизация: избегай постоянного чтения volatile
public void processData() {
    int value = hotCounter; // Один volatile read
    // Используй value вместо постоянного чтения hotCounter
    // Компилятор может оптимизировать
    for (int i = 0; i < 1000000; i++) {
        useValue(value); // Не будет переполняться каждый раз
    }
}

Итоговый чеклист

✓ Volatile используется для флагов и статусов ✓ Volatile гарантирует видимость, НЕ атомарность ✓ Для счётчиков используй AtomicInteger ✓ Для complex операций используй synchronized или Lock ✓ Volatile стоит дороже обычного поля (memory barrier) ✓ Всегда указывай volatile явно, не полагайся на "интуицию" ✓ Помни про happens-before при использовании volatile

Volatile - это мощный инструмент для многопоточности, но используй его в правильных контекстах.

Как работает Volatile? | PrepBro