В чём разница между synchronized и volatile?
synchronized и volatile — это два ключевых слова в Java для работы с многопоточностью. Они решают разные задачи:
Junior уровень
Базовое понимание
synchronized и volatile — это два ключевых слова в Java для работы с многопоточностью. Они решают разные задачи:
| Характеристика | volatile | synchronized |
|---|---|---|
| Что делает | Гарантирует видимость (запись одного потока сразу видна всем читающим, а не лежит в локальном кэше ядра) | Гарантирует видимость + атомарность (составная операция выполняется целиком, без вмешательства других потоков) |
| Где применяется | Только к полям (переменным) | К методам или блокам кода |
| Блокировка | Нет | Да (захват монитора объекта) |
| Deadlock | Невозможен | Возможен |
Пример volatile
public class Worker {
private volatile boolean running = true;
public void stop() {
running = false; // Все потоки сразу увидят это значение
}
public void doWork() {
while (running) {
// Работаем...
}
}
}
Пример synchronized
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // Атомарная операция — только один поток может выполнять
}
public synchronized int getCount() {
return count;
}
}
Когда что использовать
volatile— для флагов (stop flag, ready flag), когда один поток пишет, другие читаютsynchronized— когда нужно атомарно прочитать-изменить-записать (счётчики, составные операции)
Когда volatile НЕ подходит
- Составные операции (increment, check-then-act):
volatile count++НЕ атомарен — используйте AtomicInteger - Группы переменных: если нужно атомарно обновить два поля — volatile не поможет, нужен synchronized
- Зависимые переменные: если значение B зависит от A — volatile не гарантирует атомарность пары (A, B)
Middle уровень
Java Memory Model (JMM)
Оба механизма работают в рамках Java Memory Model (JMM), определённой в JSR-133.
Как работает volatile
При записи в volatile переменную JVM вставляет Memory Barriers:
Memory Barrier (барьер памяти / memory fence) — инструкция процессора, запрещающая переупорядочивание операций чтения/записи вокруг барьера. Без барьера процессор может переставить инструкции для оптимизации, что приведёт к некорректному поведению в многопоточной среде.
Типы барьеров в JVM:
- LoadLoad: запретить переупорядочивание двух чтений
- StoreStore: запретить переупорядочивание двух записей
- LoadStore: чтение не может быть переставлено после записи
- StoreLoad: запись не может быть переставлена после чтения (самый дорогой)
Где “Load” = чтение из памяти, “Store” = запись в память.
- Store-Store барьер — гарантирует, что все обычные записи до volatile будут сброшены в память
- Store-Load барьер — гарантирует, что последующие чтения увидят актуальные данные
// Без volatile — проблема видимости
int x = 0;
boolean ready = false; // без volatile
// Поток 1:
x = 42;
ready = true;
// Поток 2:
if (ready) {
System.out.println(x); // Может вывести 0! (x ещё не виден)
}
// С volatile — гарантия видимости
int x = 0;
volatile boolean ready = false;
// Поток 1:
x = 42;
ready = true; // Store-Load барьер — x гарантированно записан
// Поток 2:
if (ready) {
System.out.println(x); // Всегда выведет 42!
}
Happens-Before гарантия
Запись в volatile happens-before последующему чтению этой же переменной. Это означает, что все изменения, сделанные до записи в volatile, будут видны потоку, который читает эту переменную.
Как работает synchronized
synchronized обеспечивает:
- Взаимное исключение (Mutual Exclusion) — только один поток выполняет код одновременно
- Видимость (Visibility) — при входе в synchronized блок кэш инвалидируется, при выходе — данные сбрасываются в память
- Атомарность (Atomicity) — составные операции выполняются как единое целое
// Проблема: volatile НЕ обеспечивает атомарность
volatile int counter = 0;
counter++; // НЕ безопасно! Это 3 операции: read → modify → write
// Решение: synchronized обеспечивает атомарность
synchronized(lock) {
counter++; // Безопасно — только один поток выполняет
}
volatile vs AtomicBoolean: volatile гарантирует только видимость. AtomicBoolean гарантирует видимость + атомарность через CAS. Для простого флага (running = true) volatile достаточно. Для if (!flag.compareAndSet(false, true)) нужен AtomicBoolean.
Double-Checked Locking паттерн
Классический пример, где volatile критически важен:
public class Singleton {
// ОБЯЗАТЕЛЬНО volatile! Без него возможен частично инициализированный объект
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // Первая проверка (без блокировки)
synchronized (Singleton.class) {
if (instance == null) { // Вторая проверка (с блокировкой)
instance = new Singleton(); // Создание объекта
}
}
}
return instance;
}
}
Без volatile возможна ситуация, когда:
- Поток A начинает создавать
instance(выделяет память, но конструктор не завершён) - JVM переупорядочивает: сначала присваивает ссылку, потом вызывает конструктор
- Поток B видит
instance != nullи получает частично инициализированный объект
Senior уровень
Under the Hood: Реализация на уровне CPU
volatile на уровне процессора
На архитектуре x86 запись в volatile поле транслируется в инструкцию с префиксом lock, который:
Префикс lock — x86-инструкция, блокирующая кэш-линию на время операции. Другие ядра не могут читать/писать эту кэш-линию, пока операция не завершена.
- Блокирует кэш-линию на время записи
- Посылает сигнал инвалидации другим ядрам (протокол MESI)
- Запрещает переупорядочивание инструкций через барьер
Протокол MESI (Modified, Exclusive, Shared, Invalid) — протокол когерентности кэшей многоядерных CPU. Когда ядро записывает данные (Modified), оно рассылает сигнал другим ядрам — их копии становятся Invalid и должны быть перечитаны из RAM.
# Обычная запись (без volatile):
mov [rax], 1
# Volatile запись (с memory barrier):
lock or dword ptr [rax], 0 # Store barrier
mov [rax], 1
lock or dword ptr [rax], 0 # Load barrier
synchronized на уровне байт-кода
synchronized(obj) { /* code */ }
Байт-код:
aload_1 // Загружаем obj
dup
astore_2
monitorenter // Захват монитора
/* code */
aload_2
monitorexit // Освобождение монитора
Важно: компилятор генерирует два monitorexit — один для нормального выхода, второй для обработки исключения (скрытый finally).
Reordering и Memory Barriers
Процессоры и JIT-компилятор переупорядочивают инструкции для оптимизации. volatile накладывает ограничения:
| Тип барьера | Запрещает |
|---|---|
| LoadLoad | Чтение после чтения не может быть перемещено вверх |
| StoreStore | Запись после записи не может быть перемещена вверх |
| LoadStore | Запись после чтения не может быть перемещена вверх |
| StoreLoad | Чтение после записи не может быть перемещено вверх (самый дорогой) |
volatile запись = StoreStore + StoreLoad барьеры
volatile чтение = LoadLoad + LoadStore барьеры
Производительность и Highload
Cache Invalidation и False Sharing
public class Counters {
public volatile int counter1; // Могут быть в одной кэш-линии
public volatile int counter2; // Запись в counter1 инвалидирует counter2!
}
Решение — @Contended (Java 8+):
public class ContendedCounters {
@Contended
public volatile int counter1;
@Contended
public volatile int counter2; // Теперь в разных кэш-линиях
}
Адаптивный спиннинг (Adaptive Spinning)
Современная JVM не отправляет поток в BLOCKED сразу. Сначала поток выполняет spin-wait — пустой цикл, ожидая освобождения монитора. Если спиннинг оказался успешным — экономим на context switch. Если нет — паркуем поток через ОС.
Сравнительная таблица (Advanced)
| Характеристика | volatile | synchronized |
|---|---|---|
| Тип гарантии | Видимость + упорядочивание | Видимость + атомарность + исключение |
| Уровень | Переменная (поле) | Метод или блок кода |
| Блокировка | Нет (Lock-free) | Да (Blocking) |
| Кэш CPU | Отключает кэширование переменной | Сбрасывает весь локальный кэш при входе/выходе |
| Deadlock | Невозможен | Возможен |
| Overhead | Низкий (memory barrier) | Средний/высокий (зависит от contention) |
| Реентерабельность | Н/Д | Да |
Диагностика
# jstack покажет BLOCKED потоки для synchronized
jstack <pid> | grep "BLOCKED"
# Для volatile проблем — только через анализ поведения
# JCStress для стресс-тестирования
Best Practices
- volatile — для флагов и публикации immutable объектов
- synchronized — для составных операций (read-modify-write)
- Atomic* — для простых счётчиков (CAS быстрее synchronized)
- Private lock — используйте
private final Object lockвместоsynchronized(this) - Избегайте
volatile long/doubleбез необходимости атомарности (на 32-битных JVM)
🎯 Шпаргалка для интервью
Обязательно знать:
volatileгарантирует только видимость,synchronized— видимость + атомарность + mutual exclusionvolatileприменяется только к полям,synchronized— к методам и блокам кодаvolatileНЕ защищает составные операции (count++ не атомарен даже с volatile)volatileиспользует memory barriers (StoreStore, StoreLoad),synchronized— захват монитора- Deadlock невозможен с
volatile, но возможен сsynchronized - На x86
volatileзапись транслируется вlockпрефикс и инвалидацию кэш-линий (MESI) - Double-Checked Locking требует
volatileдля корректной публикации Singleton volatile— lock-free, overhead ниже чем уsynchronizedпри низкой конкуренции
Частые уточняющие вопросы:
- Можно ли использовать volatile для счётчика? — Нет, count++ = read-modify-write (3 операции), используйте AtomicInteger
- Что произойдёт без volatile в Double-Checked Locking? — JVM может переупорядочить: ссылка присвоена до завершения конструктора → частично инициализированный объект
- Какие memory barriers вставляет volatile? — Запись: StoreStore + StoreLoad; Чтение: LoadLoad + LoadStore
- Когда volatile предпочтительнее synchronized? — Для простых флагов (stop, ready), где один поток пишет, другие читают
Красные флаги (НЕ говорить):
- “Volatile делает операции атомарными” — нет, это про AtomicInteger/CAS
- “Synchronized и volatile взаимозаменяемы” — нет, они решают разные задачи
- “Volatile защищает группу переменных” — нет, только одну переменную
- “Synchronized нужен для видимости, volatile для атомарности” — ровно наоборот
Связанные темы:
- [[2. Что такое happens-before relationship]] — volatile запись happens-before чтению
- [[3. Что такое visibility problem]] — volatile решает проблему видимости
- [[8. Что такое Atomic классы]] — lock-free альтернатива для счётчиков
- [[9. Что такое CAS (Compare-And-Swap)]] — основа работы Atomic-классов