← Назад к вопросам
В чем разница между synchronized и volatile?
2.0 Middle🔥 231 комментариев
#Многопоточность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Ответ: Разница между synchronized и volatile
Оба механизма синхронизации в Java обеспечивают потокобезопасность, но работают по-разному и решают разные проблемы. Вот полное сравнение.
Основное отличие
| Параметр | synchronized | volatile |
|---|---|---|
| Что защищает | Доступ к коду/объекту | Видимость переменной |
| Мьютекс | Да, блокирует | Нет |
| Производительность | Медленнее (блокировка) | Быстрее (без блокировки) |
| Использование | Сложная логика | Простые флаги |
| Гарантирует | Атомарность + видимость | Только видимость |
volatile: Видимость между потоками
Проблема без volatile:
public class FlagExample {
private boolean flag = false; // Переменная в памяти потока (кэш)
public void setFlag() {
flag = true; // Поток 1 записывает в свой кэш
}
public void waitForFlag() {
while (!flag) { // Поток 2 читает из своего кэша, где flag = false
// Бесконечный цикл!
// Поток 2 никогда не увидит изменение flag = true
}
System.out.println("Flag is true!");
}
}
// Использование
FlagExample example = new FlagExample();
Thread thread1 = new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) {}
example.setFlag(); // Устанавливает флаг
});
Thread thread2 = new Thread(() -> {
example.waitForFlag(); // МОЖЕТ ЗАСНУТЬ ВЕЧНО!
});
thread1.start();
thread2.start();
thread2.join(); // timeout!
Решение с volatile:
public class FlagExample {
private volatile boolean flag = false; // volatile гарантирует видимость
public void setFlag() {
flag = true; // Сразу пишет в главную память
}
public void waitForFlag() {
while (!flag) { // Сразу читает из главной памяти
// Теперь Поток 2 видит изменение!
}
System.out.println("Flag is true!");
}
}
// Использование
FlagExample example = new FlagExample();
Thread thread1 = new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) {}
example.setFlag(); // Устанавливает флаг
});
Thread thread2 = new Thread(() -> {
example.waitForFlag(); // Теперь вернёт быстро!
});
thread1.start();
thread2.start();
thread2.join(); // Успешно завершится
Как работает volatile:
Без volatile:
┌─────────────┐ ┌─────────────┐
│ Поток 1 │ │ Поток 2 │
│ │ │ │
│ кэш: flag=T │ │ кэш: flag=F │ Разные кэши!
└─────────────┘ └─────────────┘
↓ ↓
[Главная память: flag=?]
С volatile:
┌─────────────┐ ┌─────────────┐
│ Поток 1 │ │ Поток 2 │
│ │ │ │
│ flag=T ───────────► [Главная память: flag=T] ◄─── flag=T
└─────────────┘ └─────────────┘
(синхронизировано)
synchronized: Взаимное исключение + видимость
Проблема без synchronized:
public class Counter {
private int count = 0; // Критичная переменная
public void increment() {
count++; // ЭТО НЕ АТОМАРНО!
}
public int getCount() {
return count;
}
}
// Использование
Counter counter = new Counter();
// 10 потоков, каждый вызовет increment() 1000 раз
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(counter.getCount()); // Ожидаем 10_000
// Выведет: 9_996 или 9_987 или другое число < 10_000
// ГОНКА ДАННЫХ! (race condition)
Почему? Потому что count++ состоит из 3 шагов:
count++ это:
1. Загрузить count из памяти (count = 5)
2. Добавить 1 (temp = 6)
3. Записать обратно (count = 6)
В многопоточной среде:
Поток 1: Загрузить count=5
Поток 2: Загрузить count=5 (видит старое значение!)
Поток 1: Добавить 1, записать count=6
Поток 2: Добавить 1, записать count=6 (потеряла инкремент потока 1!)
Результат: count=6, но должно быть 7
Решение с synchronized:
public class Counter {
private int count = 0;
public synchronized void increment() { // Блокирует весь метод
count++;
// Только один поток может быть здесь одновременно
}
public synchronized int getCount() {
return count;
}
}
// Использование (같은код)
Counter counter = new Counter();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(counter.getCount()); // 10_000 ✓ ПРАВИЛЬНО
Как работает synchronized:
Без synchronized (гонка данных):
Поток 1: ┌─ заходит ──────────────┐
│ count = 5 │
│ count = 6 │
└───────────────────────┘
Поток 2: ┌─ заходит ──────────────┐
│ count = 5 (видит 5!) │
│ count = 6 (потеря!) │
└───────────────────────┘
С synchronized (мьютекс):
Поток 1: ┌─ ЗАБЛОКИРОВАН ────────────┐
│ count = 5 │
│ count = 6 │
└────────────────────────┬─┘
Поток 2: ┌─ ЖДЁТ ─────────────────────────┐
│ (может войти когда Поток 1 │
│ выходит из synchronized) │
│ count = 6 │
│ count = 7 │
└──────────────────────────────┘
Сравнение на практике
Scenario 1: Простой флаг (volatile лучше)
public class ShutdownManager {
private volatile boolean shouldShutdown = false; // volatile достаточно
public void shutdown() {
shouldShutdown = true; // Один флаг
}
public void mainLoop() {
while (!shouldShutdown) { // Просто читаем флаг
doWork();
}
}
}
// volatile:
// + Быстро (нет блокировки)
// + Достаточно для одной переменной
Scenario 2: Сложная логика со счётчиком (synchronized нужен)
public class BankAccount {
private int balance = 1000;
// ❌ НЕПРАВИЛЬНО: volatile не поможет с гонкой данных
// private volatile int balance = 1000; // НЕПРАВИЛЬНО!
// if (balance > amount) balance -= amount; // Всё ещё не атомарно!
// ✅ ПРАВИЛЬНО: synchronized для атомарной операции
public synchronized boolean withdraw(int amount) {
if (balance >= amount) {
balance -= amount; // Две операции атомарны
return true;
}
return false;
}
public synchronized int getBalance() {
return balance;
}
}
Scenario 3: Атомарные операции (java.util.concurrent)
public class Counter {
private AtomicInteger count = new AtomicInteger(0); // Лучше всего!
public void increment() {
count.incrementAndGet(); // Атомарно, без блокировки
}
public int getCount() {
return count.get();
}
}
// Использование (同样の使用法)
Counter counter = new Counter();
executor.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(counter.getCount()); // 10_000 ✓
// Преимущества AtomicInteger:
// + Быстрее чем synchronized (использует CAS - compare-and-swap)
// + Безопаснее чем volatile
// + Специально для счётчиков
Правила выбора
Используй volatile если:
- Одна переменная
- Нужна только видимость (не атомарность)
- Нет сложной логики
private volatile boolean isRunning = true; // Флаги
private volatile int lastError = 0; // Простые значения
Используй synchronized если:
- Сложная логика (несколько операций)
- Нужна атомарность (несколько шагов должны быть неделимы)
- Нужна гарантия видимости для всех полей в объекте
public synchronized void transfer(int amount) {
// Множество операций должны быть атомарны
if (balance >= amount) {
balance -= amount;
lastTransaction = new Date();
transactionCount++;
}
}
Используй AtomicInteger/AtomicLong если:
- Простые счётчики
- Максимальная производительность
- Часто читается/пишется
private AtomicInteger requestCount = new AtomicInteger();
public void trackRequest() {
requestCount.incrementAndGet(); // Быстро и безопасно
}
Используй ReentrantLock если:
- Нужна блокировка с timeout
- Нужна справедливость (fairness)
- Нужна tryLock (неблокирующий захват)
private final Lock lock = new ReentrantLock();
public boolean tryWithTimeout() {
try {
return lock.tryLock(1, TimeUnit.SECONDS); // Ждём максимум 1 сек
} catch (InterruptedException e) {
return false;
}
}
Таблица выбора
| Задача | Инструмент | Почему |
|---|---|---|
| Флаг включения | volatile | Только видимость |
| Счётчик | AtomicInteger | Быстро и безопасно |
| Баланс счёта | synchronized | Множество операций |
| Кэш LRU | ReentrantLock | Нужна гибкость |
| Пул потоков | ConcurrentHashMap | Встроенная синхронизация |
Итоговый вывод
synchronized:
- Блокирует доступ к коду
- Гарантирует атомарность + видимость
- Медленнее, но безопаснее для сложной логики
- Используй для методов с множеством операций
volatile:
- Не блокирует
- Гарантирует только видимость
- Быстро, но только для одной переменной
- Используй для флагов и простых значений
Правило: Используй самый простой инструмент, который решает твою задачу.