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

Почему выберешь синхронизированный блок, а не метод?

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

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

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

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

Synchronized блоки vs методы: выбор правильного инструмента

Вопрос о том, когда использовать synchronized блок вместо synchronized метода, это ключевой аспект многопоточного программирования в Java. Оба подхода работают, но они имеют разные импликации для производительности и безопасности.

Основы: как работает synchronization

// Synchronized метод: синхронизирует весь метод
public synchronized void incrementCounter() {
    counter++;
}
// Эквивалентно:
public void incrementCounter() {
    synchronized (this) {
        counter++;
    }
}

// Synchronized блок: синхронизирует только часть кода
public void processData(Data data) {
    // Некритичный код, может выполняться параллельно
    validateData(data);
    
    // Только критичная секция синхронизирована
    synchronized (this) {
        saveToDatabase(data);
    }
    
    // Логирование и уведомления без блокировок
    logOperation(data);
    notifyListeners();
}

Преимущества synchronized блока

1. Гранулярность блокировки (Granular Locking)

// ❌ Плохо: весь метод заблокирован
public synchronized void processOrder(Order order) {
    Order savedOrder = validateAndSave(order);   // Может быть долгим
    sendConfirmationEmail(savedOrder);           // Может быть долгим
    updateInventory(savedOrder);                 // Может быть долгим
    notifyAnalytics(savedOrder);                 // Может быть долгим
}
// Пока один поток выполняет sendConfirmationEmail (5 сек),
// все остальные потоки ждут блокировки!

// ✅ Хорошо: синхронизирована только критичная часть
public void processOrder(Order order) {
    Order savedOrder;
    synchronized (this) {
        validateAndSave(order);  // Критичная: доступ к БД
    }
    
    // Параллельно с другими потоками
    sendConfirmationEmail(savedOrder);
    updateInventory(savedOrder);
    notifyAnalytics(savedOrder);
}

Эффект: пропускная способность (throughput) повышается в 10+ раз.

2. Различные блокировки для разных данных

public class UserService {
    private Object userLock = new Object();
    private Object orderLock = new Object();
    
    private List<User> users;
    private List<Order> orders;
    
    // ❌ Плохо: один synchronized метод блокирует всё
    public synchronized void updateUserAndOrder(User user, Order order) {
        users.add(user);      // Ждем блокировку userLock
        orders.add(order);    // Ждем блокировку orderLock
    }
    // Если другой поток модифицирует только orders,
    // он всё равно ждет блокировки users!
    
    // ✅ Хорошо: разные блокировки для разных данных
    public void updateUserAndOrder(User user, Order order) {
        synchronized (userLock) {
            users.add(user);
        }
        synchronized (orderLock) {
            orders.add(order);
        }
    }
    // Два потока могут работать одновременно:
    // один обновляет users, другой обновляет orders
}

Эффект: параллелизм повышается, меньше contention на одну блокировку.

3. Избежать блокировки некритичного кода

public class DataProcessor {
    private List<String> cache;
    private Lock cacheLock = new ReentrantLock();
    
    // ❌ Неправильно: блокируем весь метод
    public synchronized String process(String input) {
        String result = cache.get(input);
        if (result != null) {
            return result;  // Быстро, но блокировка занимает ресурсы
        }
        
        // Дорогое вычисление, блокировка удерживается
        result = expensiveComputation(input);
        cache.put(input, result);
        return result;
    }
    
    // ✅ Правильно: блокировка только для доступа к кэшу
    public String process(String input) {
        String result;
        synchronized (cacheLock) {
            result = cache.get(input);
        }
        
        if (result != null) {
            return result;  // Возвращаем без блокировки!
        }
        
        // Дорогое вычисление без блокировки
        result = expensiveComputation(input);
        
        synchronized (cacheLock) {
            cache.put(input, result);
        }
        return result;
    }
}

Эффект: блокировка удерживается минимальное время (миллисекунды, не секунды).

Когда использовать synchronized метод

// ✅ Хорошо: весь метод критичен
public synchronized void increment() {
    counter++;
}

// ✅ Хорошо: setter/getter, простые операции
public synchronized void setValue(int value) {
    this.value = value;
}

public synchronized int getValue() {
    return this.value;
}

// ✅ Хорошо: весь метод работает с одним состоянием
public synchronized void transfer(Account from, Account to, int amount) {
    from.debit(amount);
    to.credit(amount);
    // Оба счёта должны изменяться атомарно
}

Когда использовать synchronized блок

// ✅ Правильно: только часть кода критична
public void processRequest(Request req) {
    // Парсинг и валидация без блокировок
    RequestData data = parseRequest(req);
    validateData(data);
    
    // Только обновление состояния синхронизировано
    synchronized (this) {
        if (isDuplicate(data)) {
            return;  // Быстрый выход
        }
        saveToDatabase(data);
    }
    
    // Отправка уведомлений без блокировок
    sendNotification(data);
}

// ✅ Правильно: разные объекты для разных блокировок
public class ConcurrentCache<K, V> {
    private final Map<K, V> cache;
    private final Object lockA = new Object();
    private final Object lockB = new Object();
    
    public void put(K key, V value) {
        if (shouldUsePartitionA(key)) {
            synchronized (lockA) {
                cache.put(key, value);
            }
        } else {
            synchronized (lockB) {
                cache.put(key, value);
            }
        }
    }
}

Реальный пример: улучшение производительности

// Версия 1: synchronized метод
public class UserRepository {
    private List<User> users;
    
    public synchronized void saveUser(User user) {
        // Парсинг и валидация
        validateUser(user);      // 100ms
        
        // Сохранение в БД
        users.add(user);         // 1000ms
        
        // Отправка события
        notifyListeners(user);   // 500ms
    }
    // Общее время: 1600ms, блокировка удерживается 1600ms
    // Если 10 потоков: общее время = 16 сек!
}

// Версия 2: synchronized блок
public class UserRepository {
    private List<User> users;
    
    public void saveUser(User user) {
        // Парсинг и валидация БЕЗ блокировки
        validateUser(user);      // 100ms, параллельно
        
        // Сохранение в БД С блокировкой
        synchronized (this) {
            users.add(user);     // 1000ms, синхронизировано
        }
        
        // Отправка события БЕЗ блокировки
        notifyListeners(user);   // 500ms, параллельно
    }
    // Блокировка удерживается только 1000ms
    // Если 10 потоков: общее время = 1.6 сек (в 10 раз быстрее!)
}

Best practices

1. Минимизируй критичную секцию

// ❌ Плохо
synchronized (this) {
    validateInput(data);     // Не нужна синхронизация
    updateState(data);       // Нужна синхронизация
    logOperation(data);      // Не нужна синхронизация
}

// ✅ Хорошо
validateInput(data);
synchronized (this) {
    updateState(data);
}
logOperation(data);

2. Используй специализированные классы

// Вместо synchronized методов, используй thread-safe классы

// ❌ Плохо
private List<String> items;
public synchronized void add(String item) {
    items.add(item);
}

// ✅ Хорошо
private List<String> items = Collections.synchronizedList(new ArrayList<>());
public void add(String item) {
    items.add(item);
}

// ✅ Ещё лучше: CopyOnWriteArrayList для частых reads
private List<String> items = new CopyOnWriteArrayList<>();
public void add(String item) {
    items.add(item);
}

3. Предпочитай Lock/ReentrantLock для сложных сценариев

private Lock lock = new ReentrantLock();

public void criticalOperation() {
    lock.lock();
    try {
        // Критичный код
        updateState();
    } finally {
        lock.unlock();
    }
}

Производительность: реальные цифры

МетодThroughputLatency
synchronized метод100K ops/sec10ms P99
synchronized блок (гранулярный)1M ops/sec1ms P99
ConcurrentHashMap10M ops/sec0.1ms P99

Вывод

Выбирай synchronized блок вместо synchronized метода когда:

  1. Только часть метода критична — нет смысла блокировать весь метод
  2. Разные данные требуют разные блокировки — используй разные объекты блокировок
  3. Некритичный код может выполняться параллельно — тогда блокировка должна быть короче
  4. Производительность критична — synchronized блоки дают лучший throughput
  5. Нужна более гибкая синхронизация — можно использовать разные стратегии

В современной Java (для сложных случаев) предпочитай concurrent коллекции и Lock интерфейсы вместо synchronized, но понимание synchronized блоков остаётся фундаментальным знанием разработчика.

Почему выберешь синхронизированный блок, а не метод? | PrepBro