В чому різниця між synchronized методом та synchronized блоком
Ключова різниця між synchronized-методом та synchronized-блоком полягає в гранулярності (області блокування), виборі об'єкта-монітора та реалізації на рівні байт-коду:
🟢 Junior Level
Коротка відповідь для співбесіди (30 секунд)
Ключова різниця між synchronized-методом та synchronized-блоком полягає в гранулярності (області блокування), виборі об’єкта-монітора та реалізації на рівні байт-коду:
- Об’єкт блокування:
- У
synchronized-методі об’єкт монітора зафіксований мовою: це екземплярthisдля нестатичних методів або об’єкт класуClassName.classдля статичних методів. - У
synchronized-блоці розробник сам явно вказує будь-який об’єкт як монітор (рекомендуєтьсяprivate final Object lock = new Object();).
- У
- Область блокування (Scope):
- Метод блокує все своє тіло цілком від входу до виходу.
- Блок дозволяє ізолювати виключно критичну секцію, виносячи важкі обчислення, перетворення даних та I/O-операції за межі синхронізації.
- Реалізація в байт-коді:
- Метод позначається прапорцем
ACC_SYNCHRONIZEDу заголовку метода (JVM сама захоплює монітор при вході). - Блок компілюється в явні інструкції байт-коду
monitorenterтаmonitorexitз неявним блоком перехоплення винятків (Exception table).
- Метод позначається прапорцем
Порівняльна таблиця
| Критерій | synchronized метод |
synchronized блок |
|---|---|---|
| Об’єкт монітора | Неявно this або Class<?> |
Явно будь-який посилальний об’єкт |
| Гранулярність | Весь метод цілком | Довільний діапазон рядків усередині методу |
| Інкапсуляція блокування | ❌ Порушена (посилання this доступне ззовні) |
✅ Дотримана (з private final Object lock) |
| Байт-код | Прапорець ACC_SYNCHRONIZED |
Інструкції monitorenter / monitorexit |
| Гнучкість | Мінімальна | Максимальна (можна розділяти локи) |
| Ризик Deadlock | Вищий (через зовнішнє захоплення this) |
Суттєво нижчий |
Приклади на Java 21
public class SynchronizationDemo {
private final Object paymentLock = new Object();
private final Object emailLock = new Object();
private int balance = 0;
// 1. Synchronized метод (блокує весь об'єкт this)
public synchronized void updateBalance(int delta) {
this.balance += delta;
}
// 2. Synchronized блок: гранулярний захист лише критичної секції
public void processPayment(int amount, String userEmail) {
// Підготовчий крок: НЕ блокує інші потоки
validateAmount(amount);
// Критична секція: синхронізована за приватним монітором
synchronized (paymentLock) {
this.balance -= amount;
}
// I/O або мережеве відправлення: суворо ЗА межами критичної секції!
sendEmailNotification(userEmail);
}
private void validateAmount(int amount) { /* валідація */ }
private void sendEmailNotification(String email) { /* мережевий виклик */ }
}
🟡 Middle Level
Байт-код: ACC_SYNCHRONIZED проти monitorenter / monitorexit
Відмінність у реалізації на рівні віртуальної машини HotSpot принципова:
1. Метод із модифікатором synchronized:
Компілятор javac не вставляє інструкції монітора в тіло методу. Натомість у структуру method_info файлу класу записується прапорець:
public synchronized void deposit(int);
descriptor: (I)V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
0: aload_0
1: ...
5: return
Коли потік викликає метод із прапорцем ACC_SYNCHRONIZED, виконавчий механізм JVM автоматично запитує монітор неявного аргументу this (або об’єкта java.lang.Class для static). При виході з методу (за return або при викиді необробленого винятку) JVM автоматично звільняє монітор під час очищення стекового фрейму.
2. Блок synchronized (lock):
Блок транслюється в явні інструкції байт-коду:
public void withdraw(int);
Code:
0: aload_0
1: getfield #7 // Field lock:Ljava/lang/Object;
4: dup
5: astore_2
6: monitorenter // Явний вхід у монітор
7: ... // Критична секція
12: aload_2
13: monitorexit // Звільнення монітора при штатному виході
14: goto 22
17: astore_3 // Перехоплення винятку (будь-який Throwable)
18: aload_2
19: monitorexit // ГАРАНТОВАНЕ звільнення монітора при аварії!
20: aload_3
21: athrow
22: return
Exception table:
from to target type
7 14 17 any
17 20 17 any
Архітектурна вразливість synchronized(this) та перевага Private Lock
Використання synchronized-методів або synchronized(this) порушує принцип інкапсуляції. Об’єкт this публічно доступний будь-якому зовнішньому коду, який утримує на нього посилання.
Проблема 1: Зовнішнє блокування та випадковий Deadlock
// Небезпечний клас бібліотеки:
public class OrderService {
public synchronized void process() {
// Виконання бізнес-логіки
}
}
// Зовнішній шкідливий або помилковий клієнтський код:
OrderService service = new OrderService();
synchronized (service) { // Зовнішній код захопив монітор нашого об'єкта!
Thread.sleep(100_000); // Заблокував весь OrderService для всіх потоків застосунку!
}
Рішення: Патерн Private Lock Object
Створення приватного фінального об’єкта виключає можливість зовнішнього втручання:
public class SecureOrderService {
// Жоден зовнішній клас не має посилання на цей об'єкт
private final Object lock = new Object();
public void process() {
synchronized (lock) {
// Критична секція абсолютно захищена від зовнішніх захоплень
}
}
}
Гранулярність: Роздільні монітори (Lock Striping / Splitting)
synchronized-блок дозволяє розділити незалежні частини стану класу на різні монітори, збільшуючи загальну пропускну здатність:
public class MultiResourceHolder {
private final Object usersLock = new Object();
private final Object ordersLock = new Object();
private final List<String> users = new ArrayList<>();
private final List<String> orders = new ArrayList<>();
public void addUser(String user) {
synchronized (usersLock) { // Потоки, що працюють з користувачами...
users.add(user);
}
}
public void addOrder(String order) {
synchronized (ordersLock) { // ...НЕ блокують потоки, що працюють із замовленнями!
orders.add(order);
}
}
}
Якби ці методи були оголошені як synchronized void addUser() та synchronized void addOrder(), виклик addUser повністю блокував би виклик addOrder, оскільки обидва конкурували б за монітор this.
🔴 Senior Level
Успадкування та поліморфізм: втрата ACC_SYNCHRONIZED
Критичний нюанс специфікації Java (JLS §8.4.3.6): модифікатор synchronized не є частиною сигнатури методу і не успадковується підкласами при перевизначенні!
public class SuperClass {
public synchronized void doWork() {
// Потокобезпечна логіка
}
}
public class SubClass extends SuperClass {
@Override
public void doWork() {
// УВАГА: метод doWork() у SubClass НЕ синхронізований!
// Прапорець ACC_SYNCHRONIZED відсутній у method_info підкласу.
super.doWork(); // Виклик super() всередині захопить монітор this
}
}
Якщо розробник очікує, що перевизначений метод збереже потокобезпечність предка, виникає важковловимий Race Condition. Із synchronized-блоком усередині методу такої проблеми не виникає: реалізація явно видна в коді методу.
Небезпеки static synchronized та ClassLoader Hierarchy
Статичний синхронізований метод блокується на об’єкті класу ClassName.class:
public class GlobalRegistry {
public static synchronized void register(String key) { ... }
}
Підводний камінь із кількома завантажувачами класів:
- У Java унікальність об’єкта
java.lang.Classвизначається парою:Повне кваліфіковане ім'я класу + ClassLoader. - У модульних архітектурах (OSGi, плагінні системи, контейнери сервлетів/Tomcat) один і той самий клас може бути завантажений двома різними завантажувачами класів (
ClassLoader AтаClassLoader B). - У пам’яті JVM існуватимуть два фізично різних об’єкти
GlobalRegistry.class. - У результаті потоки, що звертаються до статичних синхронізованих методів через різні завантажувачі, захоплюватимуть різні монітори, і взаємне виключення буде повністю порушено!
JIT-Оптимізації: Escape Analysis та Lock Elision
Вплив вибору між методом і блоком на JIT C2-компілятор:
synchronized(this)/synchronized-метод: Об’єктthisпрактично завжди має статус витоку ArgEscape або GlobalEscape, оскільки посилання на нього передається у викликаючий контекст при створенні. JIT-компілятор не може усунути блокування наthis, навіть якщо метод викликається локально.synchronized(lock): Якщо об’єктlockінкапсульований усередині закритого контексту або створюється локально (наприклад, тимчасовий буфер), Escape Analysis доводить статусNoEscapeі JIT C2 повністю вирізає інструкції захоплення монітора зі скомпільованого машинного коду (Lock Elision).
🎯 Шпаргалка для інтерв’ю
1. 30-секундна квінтесенція (Elevator Pitch)
synchronized-метод блокує весь метод цілком на об’єкті this (або Class для static) через метадані байт-коду (ACC_SYNCHRONIZED). synchronized-блок дозволяє синхронізувати лише критичну ділянку коду на будь-якому обраному об’єкті за допомогою байткод-інструкцій monitorenter та monitorexit. Блоки з private final Object lock архітектурно переважніші: вони зберігають інкапсуляцію, запобігають зовнішнім взаємним блокуванням (Deadlock), дозволяють винести I/O за межі лока та забезпечують розділення блокувань (Lock Splitting).
2. 4 каверзних запитання з глибокими відповідями
Запитання 1: Чи успадковується модифікатор synchronized при перевизначенні методу в дочірньому класі?
Відповідь:
Ні, не успадковується. Модифікатор synchronized є деталлю внутрішньої реалізації методу, а не частиною його сигнатури чи контракту інтерфейсу. Якщо суперклас оголошує public synchronized void action(), а підклас перевизначає його як public void action(), то метод дочірнього класу буде несинхронізованим. Якщо викликати всередині super.action(), то на час виконання коду суперкласу монітор this буде захоплений, але тіло самого перевизначеного методу залишиться незахищеним від паралельних потоків.
Запитання 2: Чи можуть два потоки одночасно виконувати synchronized static void methodA() та synchronized void methodB() одного й того самого об’єкта?
Відповідь: Так, абсолютно вільно. Ці методи захоплюють абсолютно різні монітори:
synchronized static void methodA()захоплює монітор об’єктаClass(наприклад,MyClass.class).synchronized void methodB()захоплює монітор конкретного екземпляраthis. Оскільки ці два монітори ніяк не пов’язані між собою, потоки не блокуватимуть один одного. Якщо ці методи модифікують один і той самий стан (наприклад, метод екземпляра читає статичне поле), виникне стан гонки (Race Condition).
Запитання 3: Чому виклик мережевих запитів або операцій введення-виведення (I/O) всередині synchronized-методу вважається грубою помилкою?
Відповідь: Час виконання I/O-операцій (мережа, диск, база даних) недетермінований і може складати від десятків мілісекунд до хвилин при мережевих збоях. Якщо утримувати монітор під час I/O:
- Усі інші потоки, що звертаються до синхронізованих методів цього об’єкта, переходять у стан
BLOCKED. - Черги
_cxqта_EntryListстрімко розростаються, викликаючи деградацію пулів потоків застосунку (Thread Pool Starvation). - Призводить до каскадної відмови всієї системи.
Правило: всередині синхронізованого блоку повинні знаходитися виключно швидкі in-memory операції (читання/запис змінних, оновлення структур). I/O завжди виноситься до або після блоку.
Запитання 4: Як зовнішній клієнтський код може влаштувати DoS (Denial of Service) класу, що використовує synchronized-методи?
Відповідь:
Оскільки synchronized-метод блокується на this, посилання на цей об’єкт доступне викликаючому коду. Зловмисний або некоректний зовнішній код може явно виконати:
synchronized (targetInstance) {
Thread.sleep(Long.MAX_VALUE); // або будь-яка блокуюча операція
}
Оскільки монітор targetInstance захоплений зовнішнім кодом, жоден потік більше не зможе викликати жоден synchronized-метод на цьому екземплярі. Захиститися від цієї вразливості можна тільки використанням приватного внутрішнього монітора: synchronized (privateLock).
3. Типові помилки та Red Flags
❌ “Synchronized-метод працює швидше, ніж synchronized-блок, тому що в ньому немає інструкцій monitorenter/monitorexit.”
Правильно: Різниця в продуктивності на сучасних JIT-компіляторах практично нульова. Набагато більший виграш у швидкості дає зменшення часу утримання блокування, яке досягається звуженням області синхронізації через synchronized-блок.
❌ “Синхронізація за new Object() прямо всередині методу захистить дані: synchronized(new Object()) { ... }.”
Правильно: Це груба помилка. Кожен потік при вході в метод створить свій власний новий екземпляр Object і захопить його монітор. Жодного взаємного виключення між потоками не відбудеться, критична секція виконуватиметься паралельно. Об’єкт-блокування зобов’язаний бути спільним полем класу: private final Object lock = new Object();.
❌ “Статичний synchronized-метод захищає методи екземпляра того ж класу.”
Правильно: Статичні методи блокуються за Class.class, а екземплярні — за this. Це два різні монітори.