Как работает synchronized на уровне монитора?
synchronized — это ключевое слово в Java, которое обеспечивает потокобезопасность путём блокировки. Только один поток может выполнять код внутри synchronized блока одновременно.
Junior уровень
Базовое понимание
synchronized — это ключевое слово в Java, которое обеспечивает потокобезопасность путём блокировки. Только один поток может выполнять код внутри synchronized блока одновременно.
Механизм: только один поток может войти в synchronized-блок одновременно. Остальные ждут (паркуется через ОС). Когда первый поток выходит — один из ждущих получает доступ.
Два способа использования
1. synchronized метод
public class Counter {
private int count = 0;
// Блокирует весь метод на объекте 'this'
public synchronized void increment() {
count++;
}
}
2. synchronized блок
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized(lock) { // Блокирует только этот блок
count++;
}
}
}
Что блокируется?
| Тип | Объект блокировки |
|---|---|
synchronized метод экземпляра |
this (текущий объект) |
static synchronized метод |
ClassName.class (объект класса) |
synchronized(this) блок |
this (текущий объект) |
synchronized(lock) блок |
Объект lock |
Простой пример: Bank Account
public class BankAccount {
private double balance = 0;
public synchronized void deposit(double amount) {
balance += amount; // Только один поток одновременно
}
public synchronized void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
}
}
public synchronized double getBalance() {
return balance;
}
}
wait() и notify()
public class ProducerConsumer {
private final List<Integer> queue = new ArrayList<>();
private final int MAX_SIZE = 10;
public synchronized void produce(int value) throws InterruptedException {
while (queue.size() == MAX_SIZE) {
wait(); // Ждём, пока появится место
}
queue.add(value);
notifyAll(); // Уведомляем потребителей
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // Ждём, пока появятся данные
}
int value = queue.remove(0);
notifyAll(); // Уведомляем производителей
return value;
}
}
Middle уровень
Эволюция состояний блокировки
JVM (HotSpot) не всегда использует тяжёлые блокировки. Существует 4 состояния:
1. No Lock (без блокировки)
Объект не заблокирован. Mark Word (заголовок объекта) содержит:
- Identity hashcode
- Age (для GC)
2. Biased Locking (смещённая блокировка) — УДАЛЕНО в Java 21
Если объект захватывается только одним потоком, JVM запоминает этот поток:
Mark Word: [Thread ID: 54 бита] [Epoch: 2] [Age: 4] [Bias: 1] [01]
- Поток входит в synchronized без CAS-операций
- Просто проверяет: “это мой ID в заголовке?” → да → входит
- Проблема: при появлении второго потока нужно отменять (revoke) блокировку — дорого
3. Thin Lock (лёгкая блокировка)
При появлении конкуренции:
Mark Word: [Thread ID: 54 бита] [Lock Record: 6 бит] [00]
-
Используется CAS (Compare-And-Swap) для захвата
CAS (Compare-And-Swap) — атомарная операция: «запиши новое, только если текущее = ожидаемое». Используется для thin lock захвата без блокировки ОС. Подробности — в файле [[9. Что такое CAS (Compare-And-Swap)]].
-
Поток не засыпает — делает спиннинг (крутится в цикле, ожидая)
Спиннинг (spin-waiting) — поток крутится в пустом цикле, проверяя может ли захватить блокировку. Быстрее парковки (не передаёт управление ОС), но тратит CPU.
-
Быстро при низкой конкуренции
4. Fat Lock (тяжёлая блокировка)
При высокой конкуренции:
Mark Word: [Object Monitor Pointer: 62 бита] [10]
- Создаётся
ObjectMonitorв нативной памяти -
Поток паркуется через ОС (futex в Linux)
Паркировка потока — поток «засыпает» и не тратит CPU, пока ОС не разбудит его (когда блокировка освободится). Дешевле спиннинга при долгом ожидании, но дороже при коротком (context switch).
- Переходит в состояние BLOCKED
- Дорого (~1000+ ns из-за context switch)
Алгоритм захвата блокировки
Поток пытается войти в synchronized
│
▼
Это Biased Lock?
│ да │ нет
▼ ▼
Проверяем CAS-попытка
Thread ID захвата
│ │
▼ ▼
Мой ID? Успех?
│ да │ нет │ да │ нет
▼ ▼ ▼ ▼
Вхожу Revocation Вхожу Spin?
│ │
▼ ▼
Thin Lock Успех спина?
│ да │ нет
▼ ▼
Вхожу Паркуемся (Fat Lock)
Байт-код: monitorenter / monitorexit
synchronized(obj) {
// критическая секция
}
Превращается в байт-код:
0: aload_1 // Загружаем obj
1: dup // Дублируем для astore
2: astore_2 // Сохраняем для monitorexit
3: monitorenter // === ЗАХВАТ МОНИТОРА ===
4: ... // Критическая секция
N: aload_2
N+1: monitorexit // === ОСВОБОЖДЕНИЕ (нормальный выход) ===
N+2: goto end
N+3: astore_3 // Exception handler
N+4: aload_2
N+5: monitorexit // === ОСВОБОЖДЕНИЕ (при исключении) ===
N+6: aload_3
N+7: athrow
end: return
Важно: Для synchronized методов байт-код НЕ содержит monitorenter/monitorexit. Вместо этого в заголовке метода стоит флаг ACC_SYNCHRONIZED, который JVM обрабатывает автоматически.
Адаптивный спиннинг (Adaptive Spinning)
HotSpot JVM не просто крутится в цикле. Она запоминает историю:
- Если в прошлый раз спиннинг на этом мониторе удался → крутится дольше
- Если нет → сразу паркует поток
Это радикально улучшает производительность при кратковременных блокировках.
Senior уровень
Under the Hood: Реализация в HotSpot
ObjectMonitor структура (C++)
// Упрощённо из OpenJDK
class ObjectMonitor {
public:
void* _owner; // Поток-владелец (Thread*)
int _Recursions; // Счётчик реентерабельности
ObjectWaiter* _EntryList; // Очередь BLOCKED потоков
ObjectWaiter* _WaitSet; // Очередь WAITING потоков
jint _WaitSetLock; // Защита WaitSet
int _contentions; // Счётчик конкуренции
// ...
};
Lock Record на стеке потока
При Thin Locking JVM создаёт Lock Record на стеке:
Stack Frame:
┌──────────────────────────┐
│ Локальные переменные │
├──────────────────────────┤
│ Lock Record: │
│ - Displaced Mark Word │ ← Копия оригинального Mark Word
│ - Owner reference │ ← Ссылка на объект
├──────────────────────────┤
│ Frame pointer │
├──────────────────────────┤
│ Return address │
└──────────────────────────┘
Оптимизации JIT-компилятора
Lock Coarsening (Укрупнение блокировки)
// Исходный код:
synchronized(lock) { sb.append("a"); }
synchronized(lock) { sb.append("b"); }
synchronized(lock) { sb.append("c"); }
// JIT объединяет в одну блокировку:
synchronized(lock) {
sb.append("a");
sb.append("b");
sb.append("c");
}
Это уменьшает оверхед на захват/освобождение монитора.
Lock Elision (Удаление блокировки через Escape Analysis)
public String buildString() {
Object lock = new Object(); // Объект не выходит за пределы метода
synchronized(lock) {
// JIT понимает: никто другой не может получить lock
// → полностью удаляет блокировку из машинного кода!
return "result";
}
}
Escape Analysis определяет:
- Объект не возвращается из метода
- Объект не передаётся в другие методы
- Объект не сохраняется в статические поля
Производительность и Highload
Бенчмарк (примерный)
| Сценарий | Время | Примечание |
|---|---|---|
| No lock | ~1 ns | Базовая операция |
| Uncontended synchronized | ~10-20 ns | Thin lock, без конкуренции |
| Contended synchronized | ~1000-5000 ns (зависит от JVM, CPU, ОС, нагрузки — ориентируйтесь на порядок величин, не на точные числа) | Fat lock + context switch |
| Biased locking (deprecated) | ~5 ns | Только один поток |
Факторы, влияющие на производительность
- Contention Level — сколько потоков конкурируют
- Lock Hold Time — как долго держится блокировка
- Lock Frequency — как часто захватывается
- Number of Locks — количество разных блокировок
Рекомендации для Highload
// ПЛОХО: Глобальная блокировка
private static final Object globalLock = new Object();
public void process(Request r) {
synchronized(globalLock) { // Все запросы последовательно!
handle(r);
}
}
// ХОРОШО: Сегментированные блокировки
private final Object[] locks = new Object[16];
public void process(Request r) {
int segment = r.userId() % 16;
synchronized(locks[segment]) { // Параллелизм по сегментам
handle(r);
}
}
// ЛУЧШЕ: Lock-free
private final ConcurrentHashMap<Integer, Data> map = new ConcurrentHashMap<>();
public void process(Request r) {
map.compute(r.userId(), (k, v) -> handle(r, v)); // Lock-free
}
Диагностика
Java Flight Recorder (JFR)
java -XX:StartFlightRecording=filename=recording.jfr MyApp
События:
jdk.JavaMonitorEnter— время ожидания блокировкиjdk.ThreadPark— время в park()
-XX:+PrintFlagsFinal
java -XX:+PrintFlagsFinal -version | grep -i lock
Ключевые флаги:
UseBiasedLocking(удалено в Java 21)PreBlockSpin/SpinBackoffMultiplier
jstack
jstack <pid>
Ищите:
"thread-1" #10 BLOCKED (on object monitor)
- waiting to lock <0x000000076af0c8d0>
- locked <0x000000076af0c8e0>
Best Practices
- Минимизируйте synchronized блоки — только критическая секция
- Используйте private final Object lock — инкапсуляция
- Избегайте synchronized на уровне класса —
synchronized(ClassName.class) - Рассмотрите Lock Coarsening — не дробите блокировки без нужды
- Для Highload — используйте ConcurrentHashMap, LongAdder, Atomic*
- Мониторьте contention через JFR — растущее время = проблема
- Для Java < 15: отключайте biased locking в высоконагруженных системах (
-XX:-UseBiasedLocking). Для Java 15+ он уже отключён по умолчанию.
🎯 Шпаргалка для интервью
Обязательно знать:
- 4 состояния блокировки: No Lock → Biased (удалён в Java 21) → Thin Lock (CAS + спиннинг) → Fat Lock (ObjectMonitor)
- Байт-код synchronized блока: monitorenter/monitorexit; synchronized метода: флаг ACC_SYNCHRONIZED
- Два monitorexit генерируются — для нормального выхода и для exception (скрытый finally)
- Адаптивный спиннинг: JVM запоминает историю — если спиннинг удался, крутится дольше
- Lock Coarsening: JIT объединяет мелкие блокировки на одном объекте в одну большую
- Lock Elision: JIT удаляет блокировку, если Escape Analysis показывает, что объект не «убегает»
- Fat Lock = context switch через ОС (futex в Linux), ~1000-5000 ns
Частые уточняющие вопросы:
- Почему synchronized метод НЕ содержит monitorenter? — JVM обрабатывает флаг ACC_SYNCHRONIZED автоматически при вызове метода
- Что такое Lock Record? — Структура на стеке потока с копией Mark Word; используется при Thin Lock без создания ObjectMonitor
- Как JVM решает, парковать поток или спиннить? — Adaptive Spinning: смотрит историю успешных спинов на этом мониторе
- Когда JIT удалит synchronized? — Когда Escape Analysis покажет NoEscape: объект не возвращается, не передаётся, не сохраняется
Красные флаги (НЕ говорить):
- “Synchronized всегда создаёт ObjectMonitor” — нет, только при высокой конкуренции (Fat Lock)
- “Biased Locking включён в современных Java” — нет, удалён в Java 21
- “Monitorexit один на synchronized блок” — нет, два: для нормального выхода и для exception
- “Lock Elision работает для synchronized(this)” — редко, так как
thisпочти всегда «убегает»
Связанные темы:
- [[4. Что такое монитор (monitor) в Java]] — структура монитора и ObjectMonitor
- [[6. В чём разница между synchronized методом и synchronized блоком]] — ACC_SYNCHRONIZED vs monitorenter
- [[7. Что такое reentrant lock]] — реентерабельность монитора
- [[9. Что такое CAS (Compare-And-Swap)]] — CAS используется для Thin Lock захвата