Питання 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]] — коли синхронізація взагалі потрібна