Синхронизация в Java часто воспринимается как простая языковая конструкция — достаточно использовать ключевое слово synchronized, и код начинает «просто работать».
На практике же на уровне JVM происходит цепочка событий, которую можно проследить до Mark Word — восьмибайтового служебного поля заголовка каждого Java-объекта.
Современные JVM (HotSpot, OpenJ9, GraalVM) не используют фиксированную модель блокировок. Вместо этого они динамически выбирают стратегию синхронизации, исходя из реального поведения потоков и истории использования объекта.
Эта статья предназначена для Java‑разработчиков, которые уже знакомы с многопоточностью и synchronized, но хотят разобраться, как именно JVM управляет блокировками, какие состояния проходит объект и какую роль в этом играет Mark Word.
Структура Java-объекта: заголовок и Mark Word
Когда вы создаёте new Object() в Java, JVM выделяет в памяти не только область под данные объекта, но и служебный заголовок.
Одной из ключевых частей этого заголовка является Mark Word, используемый JVM для хранения метаинформации об объекте:
// Визуализация объекта в памяти (64-bit, compressed oops): Object obj = new Object(); // Адрес в памяти: 0x000000076ab0c410 // Содержимое: // [0x0000000000000001] ← Mark Word (8 байт) // [0x00000007c0060c00] ← Указатель на класс (4/8 байт) // [данные объекта...] ← Поля вашего класса
Mark Word — это служебное поле заголовка объекта, которое JVM использует для следующих задач:
синхронизация (самое важное для нас)
сборка мусора (отметка и возраст объекта)
хранение identity hash-кода (при первом вызове
System.identityHashCode());смещённая блокировка (устаревшая оптимизация)
Концептуально Mark Word можно представить как машинное слово, в котором отдельные биты кодируют состояние объекта. В 64-битных JVM это, как правило, 64-битное значение, часть которого отводится под состояние синхронизации.
Для целей синхронизации в Mark Word кодируются несколько логических состояний, из которых для синхронизации значимы следующие:
Неблокированный (unlocked) — объект не находится под монитором; самое частое состояние.
Лёгкая (thin) блокировка — один поток вошёл в
synchronizedбез конкуренции.Тяжёлая (inflated) блокировка — используется при конкуренции между потоками или при вызове
wait()/notify().Отмечен для сборки мусора (marked for GC) - временное состояние во время работы сборщика мусора.
Хэш сохранён (hash stored) - после вызова System.identityHashCode(). Обычный
hashCode()может быть переопределён и не затрагивать Mark Word.Смещённый заголовок (displaced header) - временное состояние при переходе между другими состояниями.
Ранее применялось еще и смещённая блокировка (biased locking) — историческая оптимизация, существовавшая в HotSpot до Java 18. Она позволяла «привязать» объект к конкретному потоку, но была удалена из OpenJDK из-за высокой стоимости отмены смещения в многопоточных сценариях.
Важное уточнение: Конкретные битовые паттерны (например, 01 для unlocked) зависят от реализации JVM. HotSpot, OpenJ9 и GraalVM могут использовать разные комбинации битов для одних и тех же концептуальных состояний.
Примерный вид записи в Mark Word
Ниже приведены иллюстративные примеры значений Mark Word, полученные на HotSpot JVM, конкретные значения могут отличаться в других реализациях и конфигурациях.
1. НЕБЛОКИРОВАННЫЙ (01) — "свободен для аренды" Пример: 0x0000000000000001 ↑↑ последние биты = 01 2. ЛЁГКАЯ БЛОКИРОВКА (00) — "занято, но ключ простой" Пример: 0x000070000d65f468 ↑↑ последние биты = 00 Mark Word (hex): 0x70000d65f468 Последний байт (hex): 0x68 Последний байт (bin): 01101000 Последние 2 бита: 0 3. ТЯЖЁЛАЯ БЛОКИРОВКА (10) — "занято, очередь у двери" Пример: 0x00007f8c8400d1a2 ↑↑ последние биты = 10 Mark Word (hex): 0x7f8c8400d1a2 Последние 2 бита (dec): 2 Последние 2 бита (bin): 10
Как JVM выбирает состояние блокировки
Концептуально, детали различаются между JVM (приведён упрощённый псевдокод, иллюстрирующий общую логику выбора стратегии блокировки. Это не реальный код JVM):
State selectLockingStrategy(Object obj, Thread thread) { // Проверяем, не помечен ли объект для GC if (isMarkedForGC(obj)) return State.GC_MARKED; // Проверяем, есть ли сохранённый хэш if (hasIdentityHash(obj)) return State.HASH_STORED; // Смотрим историю использования объекта LockingProfile profile = getProfile(obj); if (profile.isSingleThreaded()) { // Один поток → тонкая блокировка return attemptThinLock(obj, thread); } else if (profile.hasWaiters()) { // Есть wait() → тяжёлая блокировка return inflateToHeavyLock(obj); } else if (profile.isHighlyContended()) { // Много конкурентов → тяжёлая с оптимизациями return createAdaptiveLock(obj); } // Дефолт: начинаем с тонкой return attemptThinLock(obj, thread); }
Жизненный цикл объекта при синхронизации
Рассмотрим, как меняется состояние Mark Word в зависимости от наличия конкуренции между потоками.
Сценарий 1: Один поток, нет конкуренции
// Простейший случай — один поток синхронизируется на объекте Object lock = new Object(); // До синхронизации: // Mark Word = 0x0000000000000001 (неблокированный) synchronized(lock) { // 1. JVM проверяет последние биты: 01 // 2. Пытается сделать CAS (сравнить-и-заменить): // Было: 0x0000000000000001 // Стало: 0x000070000d65f468 // ↑ указатель на стек потока | 00 // 3. Успех! Поток вошёл в блок counter++; // Критическая секция } // После выхода: // Mark Word снова = 0x0000000000000001 // (лёгкая блокировка снимается) Что важно: В этом сценарии не создаётся Monitor! Всё работает через быструю CAS-операцию.
Сценарий 2: Появляется конкуренция
// Два потока хотят один объект Object sharedLock = new Object(); Thread t1 = new Thread(() -> { synchronized(sharedLock) { Thread.sleep(100); // Держит блокировку } }); Thread t2 = new Thread(() -> { // Пытается войти, пока t1 внутри synchronized(sharedLock) { // Здесь произойдёт инфляция — переход от легковесной блокировки (состоящей лишь из флагов в Mark Word) // к использованию полноценного объекта-монитора, способного управлять очередями. } }); t1.start(); t2.start();
При появлении конкуренции JVM вынуждена перейти от лёгкой блокировки к полноценному монитору. Процесс инфляции (упрощённо):
T2 видит: Mark Word = [...][00] (лёгкая блокировка от T1) T2 ждёт немного ("спин"), пробует снова — не получается JVM создаёт Monitor — полноценный объект-диспетчер Mark Word меняется на: [указатель на Monitor][10] T2 встаёт в очередь Monitor'а
Универсальная концепция Monitor'а
Концептуально монитор — это абстрактный механизм, обеспечивающий:
владельца (owner);
счётчик рекурсивных входов;
очереди ожидания.
Именно этот механизм и реализуют по-разному в HotSpot (ObjectMonitor), OpenJ9 (J9Monitor) и других JVM. Хотя реализации разные, примерное наполнение едино для всех JVM:
УНИВЕРСАЛЬНАЯ КОНЦЕПЦИЯ (работает везде): [Объект] → [Монитор] → { - Владелец (owner) - Счетчик рекурсий (recursions) - Очередь для synchronized (entry queue) - Очередь для wait() (wait set) }
Конкретные реализации отличаются между JVM:
языком реализации (C++ в HotSpot, C/Java-подобные структуры в других);
расположением в памяти (нативная/Java-куча);
алгоритмами управления очередями (LIFO/FIFO/адаптивные);
названием структуры (
ObjectMonitor,J9Monitorи т.д.).
Как это посмотреть в реальном коде
Пример наблюдения изменений Mark Word с помощью библиотеки JOL (Java Object Layout).
import org.openjdk.jol.info.ClassLayout; public class LockDemo { public static void main(String[] args) throws Exception { Object obj = new Object(); System.out.println("=== 1. НОВЫЙ ОБЪЕКТ ==="); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); // Mark Word: 0x0000000000000001 (неблокированный) synchronized(obj) { System.out.println("\n=== 2. В SYNCHRONIZED (1 поток) ==="); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); // Mark Word: 0x000070000d65f468 (лёгкая блокировка) } // Создаём конкуренцию Thread t1 = new Thread(() -> { synchronized(obj) { try { Thread.sleep(500); } catch (Exception e) {} } }); t1.start(); Thread.sleep(100); // Даём t1 захватить блокировку synchronized(obj) { System.out.println("\n=== 3. ПРИ КОНКУРЕНЦИИ ==="); System.out.println(ClassLayout.parseInstance(obj).toPrintable()); // Mark Word: 0x00007f8c8400d1a2 (тяжёлая блокировка) } } }
Вывод:
=== 1. НОВЫЙ ОБЪЕКТ === java.lang.Object object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x0000000000000001 (non-biasable) 8 4 (object header: class) 0xf80001e5 === 2. В SYNCHRONIZED (1 поток) === OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x000070000d65f468 (thin lock: ...) 8 4 (object header: class) 0xf80001e5 === 3. ПРИ КОНКУРЕНЦИИ === OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x00007f8c8400d1a2 (inflated) 8 4 (object header: class) 0xf80001e5
Понимание устройства Mark Word и жизненного цикла блокировок позволяет лучше интерпретировать поведение synchronized в реальных многопоточных сценариях и осознанно подходить к вопросам производительности и конкуренции потоков.
Описанные механизмы являются концептуальными и могут отличаться в деталях реализации между различными JVM.
Материал подготовлен автором telegram-канала о изучении Java.
