В чому різниця між synchronized методом та synchronized блоком?
Обидва варіанти забезпечують синхронізацію, але відрізняються тим, що саме блокується та який об'єкт використовується як блокування.
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 краще:
- Інкапсуляція — зовнішній код не може заблокувати ваш об’єкт
- Deadlock prevention — зовнішній код не може створити deadlock з вашим локом
- Starvation prevention — зовнішній код не може викликати голодування ваших методів
- Кілька локів — можна мати різні локи для різних ресурсів
Кілька локів в одному класі
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 метод — нормальний вибір
- Маленькі методи — весь метод має бути атомарним
- Внутрішні класи — немає зовнішнього доступу до
this - Немає конкуренції — тільки один потік звертається до об’єкта
Коли 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 визначає:
- GlobalEscape — об’єкт повертається або зберігається в полі → блокування НЕ видаляється
- ArgEscape — об’єкт переданий як аргумент, але не зберігається → блокування може видалитися
- 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
- Віддавайте перевагу synchronized блоку — більше контролю
- Використовуйте private final Object lock — інкапсуляція
- Мінімізуйте область блокування — не блокуйте I/O
- Уникайте synchronized(this) — хтось ззовні може заблокувати
- Уникайте static synchronized — блокування на рівні класу небезпечне
- Розгляньте ConcurrentHashMap — часто краще за synchronized HashMap
- Моніторьте 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]] — коли синхронізація взагалі потрібна