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

В чем разница между synchronized и volatile?

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

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

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

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

Ответ: Разница между synchronized и volatile

Оба механизма синхронизации в Java обеспечивают потокобезопасность, но работают по-разному и решают разные проблемы. Вот полное сравнение.

Основное отличие

Параметрsynchronizedvolatile
Что защищаетДоступ к коду/объектуВидимость переменной
МьютексДа, блокируетНет
ПроизводительностьМедленнее (блокировка)Быстрее (без блокировки)
ИспользованиеСложная логикаПростые флаги
ГарантируетАтомарность + видимостьТолько видимость

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 если:

  1. Одна переменная
  2. Нужна только видимость (не атомарность)
  3. Нет сложной логики
private volatile boolean isRunning = true;  // Флаги
private volatile int lastError = 0;         // Простые значения

Используй synchronized если:

  1. Сложная логика (несколько операций)
  2. Нужна атомарность (несколько шагов должны быть неделимы)
  3. Нужна гарантия видимости для всех полей в объекте
public synchronized void transfer(int amount) {
    // Множество операций должны быть атомарны
    if (balance >= amount) {
        balance -= amount;
        lastTransaction = new Date();
        transactionCount++;
    }
}

Используй AtomicInteger/AtomicLong если:

  1. Простые счётчики
  2. Максимальная производительность
  3. Часто читается/пишется
private AtomicInteger requestCount = new AtomicInteger();
public void trackRequest() {
    requestCount.incrementAndGet();  // Быстро и безопасно
}

Используй ReentrantLock если:

  1. Нужна блокировка с timeout
  2. Нужна справедливость (fairness)
  3. Нужна 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Множество операций
Кэш LRUReentrantLockНужна гибкость
Пул потоковConcurrentHashMapВстроенная синхронизация

Итоговый вывод

synchronized:

  • Блокирует доступ к коду
  • Гарантирует атомарность + видимость
  • Медленнее, но безопаснее для сложной логики
  • Используй для методов с множеством операций

volatile:

  • Не блокирует
  • Гарантирует только видимость
  • Быстро, но только для одной переменной
  • Используй для флагов и простых значений

Правило: Используй самый простой инструмент, который решает твою задачу.

В чем разница между synchronized и volatile? | PrepBro