Вопрос 6 · Раздел 9

В чём разница между synchronized методом и synchronized блоком?

Оба варианта обеспечивают синхронизацию, но отличаются тем, что именно блокируется и какой объект используется как блокировка.

Версии по языкам: English Russian Ukrainian

Junior уровень

Базовое понимание

Оба варианта обеспечивают синхронизацию, но отличаются тем, что именно блокируется и какой объект используется как блокировка.

synchronized метод

public class Counter {
    private int count = 0;

    // Блокирует ВЕСЬ метод
    // Объект блокировки: this (текущий экземпляр)
    public synchronized void increment() {
        count++;
    }

    // Статический метод
    // Объект блокировки: Counter.class (объект класса)
    public static synchronized void resetAll() {
        // ...
    }
}

synchronized блок

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        // Блокирует ТОЛЬКО этот блок
        // Объект блокировки: любой указанный объект
        synchronized(lock) {
            count++;
        }
    }
}

Сравнительная таблица

Характеристика synchronized метод synchronized блок
Объект блокировки Всегда this (или Class) Любой объект
Область блокировки Весь метод Только указанный блок
Читаемость Выше (декларативно) Ниже (больше вложенности)
Гибкость Низкая Высокая
Безопасность Ниже (любой внешний код может захватить this и заблокировать метод) Выше (приватный объект-лок недоступен снаружи, меньше риск deadlock)

Когда что использовать

  • Метод — когда весь метод должен быть атомарным и нет проблем с внешним доступом к this
  • Блок — когда только часть метода требует синхронизации, или нужен контроль над объектом блокировки

Middle уровень

Различия на уровне байт-кода

synchronized блок

Использует явные инструкции monitorenter и monitorexit:

public void increment() {
    synchronized(lock) {
        count++;
    }
}

Байт-код:

public void increment();
  Code:
     0: aload_0
     1: getfield      #2  // Field lock:Ljava/lang/Object;
     4: dup
     5: astore_1
     6: monitorenter        // ← ЗАХВАТ
     7: aload_0
     8: dup
     9: getfield      #3    // Field count:I
    12: iconst_1
    13: iadd
    14: putfield      #3    // Field count:I
    17: aload_1
    18: monitorexit         // ← ОСВОБОЖДЕНИЕ (нормальный выход)
    19: goto          27
    22: astore_2
    23: aload_1
    24: monitorexit         // ← ОСВОБОЖДЕНИЕ (при исключении)
    25: aload_2
    26: athrow
    27: return

synchronized метод

Использует флаг ACC_SYNCHRONIZED в атрибутах метода:

public synchronized void increment() {
    count++;
}

Байт-код (обратите внимание — НЕТ monitorenter/monitorexit!):

public synchronized void increment();
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // ← Флаг синхронизации
  Code:
    stack=3, locals=1, args_size=1
       0: aload_0
       1: dup
       2: getfield      #2  // Field count:I
       5: iconst_1
       6: iadd
       7: putfield      #2  // Field count:I
      10: return

JVM автоматически захватывает и освобождает монитор при вызове метода с этим флагом.

ACC_SYNCHRONIZED — флаг в байт-коде (ACC = access flags). JVM видит этот флаг и автоматически оборачивает вызов метода в monitor enter/exit.

Гранулярность (Scope)

// ПЛОХО: synchronized метод блокирует весь метод
public synchronized void processOrder(Order order) {
    validate(order);        // Не требует блокировки
    calculatePrice(order);  // Не требует блокировки
    updateInventory(order); // Требует блокировки
    sendNotification(order);// Не требует блокировки (I/O операция!)
}

// ХОРОШО: synchronized блок только для критической секции
public void processOrder(Order order) {
    validate(order);         // Без блокировки
    calculatePrice(order);   // Без блокировки

    synchronized(inventoryLock) {
        updateInventory(order); // Только это блокируется
    }

    sendNotification(order); // Без блокировки
}

Приватные локи (Private Locks)

public class SafeCounter {
    // ПЛОХО: кто-то снаружи может сделать synchronized(counter)
    public synchronized void increment() {
        count++;
    }

    // ХОРОШО: приватный лок — никто снаружи не захватит
    private final Object lock = new Object();

    public void increment() {
        synchronized(lock) {
            count++;
        }
    }
}

Почему private lock лучше:

  1. Инкапсуляция — внешний код не может заблокировать ваш объект
  2. Deadlock prevention — внешний код не может создать deadlock с вашим локом
  3. Starvation prevention — внешний код не может вызвать голодание ваших методов
  4. Несколько локов — можно иметь разные локи для разных ресурсов

Несколько локов в одном классе

public class UserProfileCache {
    private final Map<Integer, Profile> profiles = new HashMap<>();
    private final Map<Integer, Avatar> avatars = new HashMap<>();

    // Отдельные локи для независимых ресурсов
    private final Object profilesLock = new Object();
    private final Object avatarsLock = new Object();

    public Profile getProfile(int id) {
        synchronized(profilesLock) {
            return profiles.get(id);
        }
    }

    public Avatar getAvatar(int id) {
        synchronized(avatarsLock) {
            return avatars.get(id);
        }
    }

    // Оба локи одновременно
    public void clearAll() {
        synchronized(profilesLock) {
            synchronized(avatarsLock) {
                profiles.clear();
                avatars.clear();
            }
        }
    }
}

Когда synchronized метод — нормальный выбор

  1. Маленькие методы — весь метод должен быть атомарным
  2. Внутренние классы — нет внешнего доступа к this
  3. Нет конкуренции — только один поток обращается к объекту

Когда synchronized блок лучше: когда нужно заблокировать только часть метода, или когда нужен отдельный объект-лок.


Senior уровень

Under the Hood: Флаг ACC_SYNCHRONIZED

synchronized метод НЕ содержит инструкций monitorenter/monitorexit в байт-коде. Вместо этого JVM проверяет флаг при вызове метода:

// JVM псевдокод при вызове метода:
if (method.hasFlag(ACC_SYNCHRONIZED)) {
    monitor.enter(method.isStatic() ? clazz : receiver);
    try {
        invokeMethod();
    } finally {
        monitor.exit(method.isStatic() ? clazz : receiver);
    }
}

Это работает незначительно быстрее (разница ~0-5%, на практике незаметна), так как не требует дополнительных инструкций в теле метода.

Оптимизации JIT-компилятора

Lock Coarsening (Укрупнение)

// До оптимизации:
for (int i = 0; i < items.size(); i++) {
    synchronized(lock) {
        total += items.get(i).price;
    }
}

// После Lock Coarsening:
synchronized(lock) {
    for (int i = 0; i < items.size(); i++) {
        total += items.get(i).price;
    }
}

JIT объединяет множество блокировок на одном объекте в одну большую.

Важно: Это работает только если JIT может доказать, что внутри цикла нет точек, где другой поток мог бы увидеть промежуточное состояние.

Lock Elision (Удаление)

public void process() {
    // Этот объект НЕ выходит за пределы метода
    StringBuilder sb = new StringBuilder();
    synchronized(sb) { // JIT удалит эту блокировку!
        sb.append("data");
    }
}

Escape Analysis определяет:

  1. GlobalEscape — объект возвращается или сохраняется в поле → блокировка НЕ удаляется
  2. ArgEscape — объект передан как аргумент, но не сохраняется → блокировка может удалиться
  3. NoEscape — объект полностью локальный → блокировка удаляется

Для synchronized(this) это работает реже, так как this почти всегда “утекает”.

Сравнительная таблица (Advanced)

Характеристика synchronized метод synchronized блок
Байт-код Флаг ACC_SYNCHRONIZED Инструкции monitorenter/exit
Объект лока Всегда this или Class Любой объект (лучше private final)
Надёжность Низкая (публичный лок) Высокая (инкапсулированный лок)
Lock Elision Работает редко Работает часто
Granularность Весь метод Только критическая секция
I/O внутри Блокирует на время I/O Можно вынести I/O за блокировку

Производительность и Highload

Avoid Static Synchronized

// ОЧЕНЬ ПЛОХО: блокировка на уровне класса
public class Config {
    private static Map<String, String> settings = new HashMap<>();

    public static synchronized void set(String key, String value) {
        settings.put(key, value); // Блокирует ВСЕХ, кто использует Config.class
    }
}

Это самый опасный вид синхронизации — может парализовать всю систему.

Решение:

public class Config {
    private static final Map<String, String> settings = new ConcurrentHashMap<>();
    private static final Object settingsLock = new Object();

    public static void set(String key, String value) {
        settings.put(key, value); // ConcurrentHashMap — lock-free
    }
}

Влияние на производительность

Операция Время Примечание
Метод (uncontended) ~10-20 ns ACC_SYNCHRONIZED overhead
Блок (uncontended) ~10-20 ns monitorenter/exit
Метод (contended) ~1000+ ns Context switch
Блок (contended) ~1000+ ns Context switch
Блок (private lock) ~10-20 ns Меньше contention

Диагностика

javap -v

javap -v MyClass.class

Для метода:

public synchronized void method();
  descriptor: ()V
  flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED  // ← Флаг здесь

Для блока:

public void method();
  Code:
    ...
    monitorenter   // ← Инструкция здесь
    ...
    monitorexit    // ← И здесь

Thread Dumps

"thread-1" BLOCKED on object monitor
  - waiting to lock <0x000000076af0c8d0> (a com.example.MyClass)
  // Для метода: видно, что это this (MyClass)
  // Для блока: видно конкретный объект блокировки

Best Practices

  1. Предпочитайте synchronized блок — больше контроля
  2. Используйте private final Object lock — инкапсуляция
  3. Минимизируйте область блокировки — не блокируйте I/O
  4. Избегайте synchronized(this) — кто-то снаружи может заблокировать
  5. Избегайте static synchronized — блокировка на уровне класса опасна
  6. Рассмотрите ConcurrentHashMap — часто лучше synchronized HashMap
  7. Мониторьте contention — если растёт, переходите на сегментированные локи

🎯 Шпаргалка для интервью

Обязательно знать:

  • synchronized метод использует флаг ACC_SYNCHRONIZED, блок — инструкции monitorenter/monitorexit
  • synchronized метод блокирует весь метод на this (или Class для static); блок — только указанную часть на любом объекте
  • Private lock (private final Object lock) безопаснее: внешний код не может захватить ваш лок и создать deadlock
  • JIT чаще применяет Lock Elision к synchronized блокам (локальный объект), чем к synchronized(this)
  • Granularity: блок позволяет вынести I/O и долгие операции за пределы синхронизации
  • Производительность uncontended одинакова (~10-20 ns), разница в гибкости и безопасности

Частые уточняющие вопросы:

  • Почему synchronized(this) опасен? — Любой внешний код может сделать synchronized(obj) и заблокировать ваши методы
  • Можно ли иметь несколько локов в одном классе? — Да, разные private final Object для разных ресурсов (профили, аватары и т.д.)
  • Когда synchronized метод — нормальный выбор? — Маленькие методы, весь метод атомарный, нет внешнего доступа к this
  • Почему static synchronized опасен? — Блокировка на уровне класса (ClassName.class) — одна блокировка на ВСЕ экземпляры

Красные флаги (НЕ говорить):

  • “Synchronized метод быстрее блока” — нет, производительность одинакова при uncontended
  • “Synchronized(this) безопасен, если класс внутренний” — в общем случае нет, инкапсуляция нарушается
  • “Monitorenter есть в synchronized методе” — нет, там флаг ACC_SYNCHRONIZED
  • “Private lock нужен только для красоты” — нет, он предотвращает external deadlock и starvation

Связанные темы:

  • [[4. Что такое монитор (monitor) в Java]] — механизм intrinsic монитора
  • [[5. Как работает synchronized на уровне монитора]] — эволюция состояний блокировки
  • [[7. Что такое reentrant lock]] — ReentrantLock как более гибкая альтернатива
  • [[1. В чём разница между synchronized и volatile]] — когда синхронизация вообще нужна