Ключевое слово volatile в Java указывает, что значение переменной может изменяться разными потоками. Объявление переменной как volatile гарантирует, что поток читает её актуальное значение, а не использует устаревшую копию из локального кэша. В многопоточной среде это важно: без такой гарантии один поток может не заметить изменение, сделанное другим.
Зачем нужен volatile?
Для ускорения работы каждый поток и процессор могут хранить значения переменных в регистрах или кэше. В результате поток иногда продолжает работать со старым значением, хотя другой поток уже записал новое. volatile устанавливает правила видимости: запись публикуется для остальных потоков, а последующее чтение получает обновлённое значение.
Основные свойства volatile
- Видимость. Изменения
volatile-переменной становятся видимыми другим потокам. Запись не может остаться только в локальном представлении одного потока. - Атомарность.
volatileне делает составные операции атомарными. Например,counter++включает чтение, увеличение и запись, поэтому два потока могут потерять одно из обновлений. - Порядок операций. Java Memory Model ограничивает переупорядочивание чтений и записей вокруг
volatile. Запись в такую переменную happens-before последующего чтения того же значения другим потоком.
Когда использовать volatile
- Когда одна простая переменная используется несколькими потоками.
- Когда читатель должен видеть последнее значение, опубликованное другим потоком.
- Когда состояние изменяется одиночной записью и не участвует в операциях read-modify-write, check-then-act или в сложных инвариантах.
Типичные примеры — флаг остановки, признак готовности или ссылка на неизменяемую конфигурацию.
Пример использования volatile
public class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true; // запись в volatile-переменную
}
public void reader() {
if (flag) { // чтение volatile-переменной
System.out.println("Flag is true!");
}
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
Thread writerThread = new Thread(() -> {
try {
Thread.sleep(1000); // имитация работы
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
example.writer();
System.out.println("Flag set to true by writer thread");
});
Thread readerThread = new Thread(() -> {
while (!example.flag) {
// ожидаем, пока значение станет true
}
example.reader();
});
writerThread.start();
readerThread.start();
}
}
В этом примере поле flag объявлено как volatile. Поток-писатель ждёт одну секунду и присваивает ему true. Поток-читатель проверяет флаг в цикле и благодаря правилам видимости замечает обновление. После этого он выходит из цикла и вызывает reader().
Без volatile Java Memory Model не обязывает поток-читатель повторно получать значение из общей памяти. Теоретически он может продолжать видеть false и ждать бесконечно. Сам цикл активного ожидания при этом не является хорошим решением для реального приложения — он лишь наглядно показывает проблему видимости.
Ограничения volatile
- Нет атомарности составных действий. Инкремент, сравнение с последующей записью или обновление нескольких полей требуют других средств синхронизации.
- Нельзя защитить сложное состояние. Если корректность зависит от согласованности нескольких значений, применяйте
synchronized,ReentrantLockили потокобезопасную структуру данных. - Не заменяет координацию потоков. Для ожидания событий лучше подходят
CountDownLatch, условия блокировок, очереди или другие высокоуровневые примитивы.
Итоги
volatile — компактный инструмент для публикации простого состояния между потоками. Он гарантирует видимость и определённый порядок операций, но не превращает составные действия в атомарные. Для флагов и независимых присваиваний этого часто достаточно; для счётчиков и сложных переходов состояния нужны атомарные классы, блокировки или синхронизированные секции. Правильный выбор механизма делает многопоточный код одновременно корректным и понятным.