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

Нужно ли использовать synchronized с volatile?

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

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

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

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

Нужно ли использовать synchronized с volatile?

Нет, они решают разные проблемы. Это частая ошибка. Давай разберёмся, когда использовать что.

Проблема: Memory Visibility

В многопоточной среде один поток может не видеть изменения другого потока:

public class VisibilityProblem {
    private int flag = 0;
    
    public void writer() {
        flag = 1;  // Поток A пишет значение
    }
    
    public void reader() {
        while (flag == 0) {  // Поток B может НЕ видеть изменение!
            // spin-wait
        }
    }
}

Чтобы B видел изменения A, нужно volatile:

public class VisibilityFixed {
    private volatile int flag = 0;  // Гарантирует visibility
    
    public void writer() {
        flag = 1;  // Видимо для всех потоков
    }
    
    public void reader() {
        while (flag == 0) {  // Теперь видит изменение
            // spin-wait
        }
    }
}

Проблема: Race Conditions

synchronized защищает от race conditions (несколько потоков одновременно меняют state):

public class Counter {
    private int count = 0;
    
    // ❌ Проблема: race condition
    public void increment() {
        count++;  // 3 шага:
                  // 1. read count
                  // 2. increment
                  // 3. write count
                  // Thread A и B могут перемешаться!
    }
    
    // ✅ Решение: synchronized
    public synchronized void increment() {
        count++;  // Только один поток в раз
    }
    
    public synchronized int getCount() {
        return count;
    }
}

Таблица: Когда что использовать

ПроблемаРешениеПример
Memory visibilityvolatileфлаги, конфигурация
Race conditionssynchronizedсчётчики, collections
Race conditionsLockболее гибкий синхронизм
Bothsynchronized volatileредко нужно вместе

synchronized vs volatile

// ❌ НЕПРАВИЛЬНО: volatile с synchronized не имеет смысла
public class WrongUsage {
    private volatile int count = 0;
    
    public synchronized void increment() {
        count++;  // synchronized уже гарантирует visibility
                  // volatile здесь лишний
    }
}

// ✅ ПРАВИЛЬНО: выбери один
public class RightUsage {
    // Вариант 1: Только synchronized для операций
    private int count = 0;
    public synchronized void increment() { count++; }
    
    // Вариант 2: Только volatile для простых флагов
    private volatile boolean running = true;
    public void stop() { running = false; }
}

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

1. Volatile для флагов

public class ThreadPool {
    private volatile boolean shutdown = false;  // Один раз пишется, много раз читается
    
    public void worker() {
        while (!shutdown) {
            // работа
        }
    }
    
    public void shutdown() {
        shutdown = true;  // Видимо для всех worker потоков
    }
}

2. Synchronized для операций

public class SafeCounter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;  // Гарантирует:
                  // - Только один поток
                  // - Visibility для следующего потока
    }
    
    public synchronized int getCount() {
        return count;
    }
}

3. Lock для fine-grained control

public class ReadWriteExample {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private Map<String, String> data = new HashMap<>();
    
    // Много потоков могут одновременно читать
    public String get(String key) {
        lock.readLock().lock();
        try {
            return data.get(key);
        } finally {
            lock.readLock().unlock();
        }
    }
    
    // Только один поток может писать
    public void put(String key, String value) {
        lock.writeLock().lock();
        try {
            data.put(key, value);
        } finally {
            lock.writeLock().unlock();
        }
    }
}

Memory Model в Java

Что гарантирует synchronized:

public synchronized void method() {
    // BEFORE: happens-before с предыдущим synchronized на этом объекте
    // Видишь все изменения, сделанные в других потоках
    
    // WORK: критическая секция
    
    // AFTER: happens-before с следующим synchronized на этом объекте
    // Твои изменения видны другим потокам
}

Что гарантирует volatile:

public volatile int value;

// WRITE: Когда пишешь в volatile
value = 42;
// ↓ happens-before
// ↓ Все изменения памяти видны другим потокам

// READ: Когда читаешь volatile
int x = value;  // ↑ Видишь все изменения других потоков

Практический сценарий: Правильный стиль

public class DataHolder {
    // volatile для простых флагов
    private volatile boolean initialized = false;
    private volatile String status = "IDLE";
    
    // synchronized для операций на mutable data
    private final List<String> items = new ArrayList<>();
    
    public void init() {
        // Инициализация
        status = "INITIALIZING";
        // ...
        status = "INITIALIZED";
        initialized = true;  // Видимо всем потокам
    }
    
    public synchronized void addItem(String item) {
        items.add(item);  // Гарантирует visibility + atomic операция
    }
    
    public synchronized List<String> getItems() {
        return new ArrayList<>(items);  // Копируем для safety
    }
    
    public boolean isInitialized() {
        return initialized;  // Просто читаем флаг
    }
}

Когда НУЖНО вместе

Редко, но есть сценарии:

public class SpecialCase {
    // Заменяемое значение, которое часто читается
    private volatile Configuration config;
    
    public void updateConfig(Configuration newConfig) {
        synchronized (this) {
            // Проверяем и обновляем атомарно
            if (newConfig.isValid()) {
                config = newConfig;  // Volatile помогает visibility
            }
        }
    }
    
    public Configuration getConfig() {
        return config;  // Быстрое чтение без lock
    }
}

Но это редко. Обычно лучше:

// ✅ Лучше: AtomicReference вместо volatile + synchronized
public class BetterApproach {
    private final AtomicReference<Configuration> config = 
        new AtomicReference<>();
    
    public void updateConfig(Configuration newConfig) {
        if (newConfig.isValid()) {
            config.set(newConfig);  // Атомарно и thread-safe
        }
    }
    
    public Configuration getConfig() {
        return config.get();  // Быстрое чтение
    }
}

Concurrent Collections

Лучше использовать готовые concurrent структуры:

// ❌ Не делай так
public class BadStyle {
    private volatile List<String> items = new ArrayList<>();
    
    public synchronized void addItem(String item) {
        items.add(item);  // items = новый ArrayList()
    }
}

// ✅ Делай так
public class GoodStyle {
    private List<String> items = new CopyOnWriteArrayList<>();
    
    public void addItem(String item) {
        items.add(item);  // Thread-safe, без synchronized
    }
}

// Или
public class MapStyle {
    private Map<String, String> cache = new ConcurrentHashMap<>();
    
    public void put(String key, String value) {
        cache.put(key, value);  // Thread-safe
    }
}

Best Practices

Используй volatile только для простых флагов

private volatile boolean running = true;
private volatile String status = "IDLE";

Используй synchronized для операций на объектах

synchronized void modify() {
    // комплексная операция
}

Используй Lock для fine-grained control

ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();  // Many readers
lock.writeLock().lock(); // Exclusive writer

Используй Atomic для простых значений*

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();  // Атомарно

Используй Concurrent Collections

List<String> items = new CopyOnWriteArrayList<>();
Map<String, String> cache = new ConcurrentHashMap<>();

НЕ смешивай без разумной причины

// Плохо
private volatile synchronized int value;  // Ошибка синтаксиса

// Плохо
private volatile int count = 0;
public synchronized void increment() {
    count++;  // volatile не нужен
}

Когда выбирать что

Я пишу один раз, много читаю?
→ volatile

Несколько потоков меняют объект?
→ synchronized или Lock

Много читающих, мало пишущих?
→ ReadWriteLock

Просто счётчик?
→ AtomicInteger

Collection со многими потоками?
→ ConcurrentHashMap, CopyOnWriteArrayList

TL;DR

ЧтоДля чегоПример
volatileMemory visibilityфлаги, конфигурация
synchronizedMutual exclusion + visibilityсчётчики, операции
LockFine-grained controlReadWriteLock
Atomic*Simple valuesAtomicInteger
ConcurrentCollectionsConcurrentHashMap

Ответ: НЕ нужно использовать synchronized с volatile на одном поле. Это признак неправильного дизайна.

Выбери один инструмент:

  • Для флагов: volatile
  • Для операций: synchronized
  • Для сложности: Lock
  • Для боли: Concurrent структуры

Помни: в modern Java часто нужны concurrent collections, а не примитивные synchronized!