В чём разница между 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]] — когда синхронизация вообще нужна