В чём разница между 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. Это два разных монитора.