Понимание ключевого слова Synchronized из заголовка объекта

интервью Java задняя часть JVM

Цель написания этой статьи исходит из того, что я прочитал «Искусство параллельного программирования», в книге говорилось, что блокировка ключевого слова synchronized помещается в заголовок объекта. Просто ответьте на этот вопрос, чтобы разобраться в содержании, связанном с этим ключевым словом.

Что такое синхронизированное ключевое слово?

Ключевое слово synchronized — очень важный инструмент в параллельном программировании на Java. Его основная цель — разрешить только одному потоку доступ к определенному фрагменту кода в каждый момент времени, тем самым защищая некоторые переменные или данные от изменения другими потоками. Такое ощущение, что группа людей спешит в туалет, и вам посчастливилось схватить его, защелкнуть дверь и запереть дверь, квадрат туалета в это время только ваш, даже если люди снаружи дверная очередь на Средиземное море (Ситуация, когда кто-то насильно снес унитаз здесь исключена).

После использования ключевого слова synchronized объект используется как замок.Как правило, существуют следующие три формы реализации.

  1. Для синхронизированных методов блокировкой является текущий объект экземпляра.
public synchronized void test1() {
    i++;
}
  1. Для статических синхронизированных методов блокировкой является объект Class текущего класса.
public static synchronized void test2() {
    i++;
}
  1. Для блоков синхронизированного кода блокировкой является объект в квадратных скобках синхронизированного ключевого слова.
public void test2() {
    synchronized(this){
        i++;
    }
}
Что такое заголовок объекта?

В JVM расположение объектов в памяти разделено на три блока: заголовок объекта, данные экземпляра и заполнение выравнивания. Давайте сначала поговорим об экземплярных данных, которые хранят реальную эффективную информацию об объекте (содержимое различных типов полей, определенных в программном коде), будь то поле, унаследованное от родительского класса или определенное в подклассе. Затем есть отступ выравнивания, который не имеет особого значения и просто действует как заполнитель. Причина в том, что JVM требует, чтобы начальный адрес объекта был целым числом, кратным 8 байтам (размер объекта должен быть целым числом, кратным 8 байтам). Заголовок объекта уже представляет собой целое число, кратное 8. Если данные экземпляра не выровнены, для его завершения требуется дополнение выравнивания.

Вот в чем дело,Блокировки, используемые синхронизацией, помещаются в заголовок объекта., JVM использует 2 слова для хранения заголовка объекта (если объект является массивом, выделяется 3 слова, и еще одно слово используется для хранения длины массива). Заголовок объекта содержит две части информации, а именно Mark Word и указатель типа. Mark Word в основном используется для хранения данных времени выполнения самого объекта, таких как хэш-код объекта, возраст генерации GC, флаг состояния блокировки, блокировка, удерживаемая потоком, идентификатор смещенного потока и смещенный метка времени. Указатель типа используется для идентификации JVM с помощью этого указателя, чтобы определить, экземпляром какого класса является объект.

Поскольку объектам необходимо хранить слишком много данных во время выполнения, Mark Word спроектирован как нефиксированная структура данных, чтобы хранить больше информации в очень маленьком пространстве. В разных состояниях объекта Mark Word будет хранить разный контент (только схему 32-битной виртуальной машины).

статус блокировки 25bit 4bit 1 бит (будь то блокировка смещения) 2 бита (флаг блокировки)
безблокировочное состояние хэш-код объекта возраст генерации объекта 0 01
Блокировка смещения идентификатор потока + эпоха возраст генерации объекта 1 01

статус блокировки 30bit 2 бита (флаг блокировки)
Легкий замок указатель на запись блокировки в стеке 00
Тяжелый замок (синхронный) указатель на мьютекс (тяжеловесная блокировка) 10
Флаг GC нулевой 11
объект наблюдения

Здесь мы в основном анализируем тяжеловесную блокировку, бит флага равен 10, указатель указывает на начальный адрес объекта-монитора, и с каждым объектом связан монитор. В Hot Spot монитор реализуется классом ObjectMonitor. Давайте сначала посмотрим на структуру данных ObjectMonitor.

ObjectMonitor() {
    _header       = NULL;//markOop对象头
    _count        = 0;
    _waiters      = 0,//等待线程数
    _recursions   = 0;//重入次数
    _object       = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
    _owner        = NULL;//指向获得ObjectMonitor对象的线程或基础锁
    _WaitSet      = NULL;//处于wait状态的线程,会被加入到waitSet;
    _WaitSetLock  = 0;
    _Responsible  = NULL;
    _succ         = NULL;
    _cxq          = NULL;
    FreeNext      = NULL;
    _EntryList    = NULL;//处于等待锁block状态的线程,会被加入到entryList;
    _SpinFreq     = 0;
    _SpinClock    = 0;
    OwnerIsThread = 0;
    _previous_owner_tid = 0;//监视器前一个拥有者线程的ID
}

Есть две очереди _EntryList и _WaitSet, которые используются для сохранения списка объектов ObjectMonitor, а _owner указывает на поток, содержащий объект ObjectMonitor. Когда несколько потоков обращаются к коду синхронизации, поток попадает в область _EntryList.После того, как поток получает монитор объекта (приоритет потока получения блокировки еще предстоит изучить), он входит в область _Owner и указывает _owner на поток, получивший блокировку ( объект монитора удерживается потоком), _count++ и другие потоки продолжают ожидать в области _EntryList. Если поток вызывает метод ожидания, он входит в область _WaitSet и ожидает пробуждения. После выполнения потока блокировка монитора снимается, а значение в ObjectMonitor сбрасывается.Как упоминалось выше, блокировки, используемые synchronized, помещаются в заголовок объекта, который, вероятно, относится к адресу памяти объекта монитора, на который указывает указатель на мьютекс в Mark Word.Из вышеизложенного мы можем понять, почему каждый объект в Java можно использовать в качестве объекта блокировки.

директива монитора

JVM синхронизирует методы и блоки кода, вводя и закрывая объекты монитора, но детали реализации различаются. Вы можете использовать команду javap -verbose XXX.class, чтобы увидеть, как достигается синхронизация после компиляции кода в байт-код.

 Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_0
         5: dup
         6: getfield      #2                  // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field i:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return

После декомпиляции кода, содержащего блок синхронизированного кода, вы увидите две инструкции monitorenter и monitorexit. monitorenter находится в начале блока кода, а monitorexit соответствует концу кода или исключения. Любой объект имеет соответствующий ему монитор, и когда монитор удерживается, он блокируется. Когда поток выполняет команду monitorenter, он пытается получить блокировку объекта (т. е. права собственности на монитор).

public synchronized void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10

Синхронизация метода является неявной, и JVM использует ACC_SYNCHRONIZED флага доступа к методу в данных типа method_info для различения. Когда поток выполняет код, если он обнаруживает, что во флаге доступа к методу есть ACC_SYNCHRONIZED, текущий поток удерживает объект монитора. Детали следующего выполнения такие же, как и для синхронизированного блока кода. Вышеизложенное является основным принципом метода синхронизации, модифицированного ключевым словом synchronized, и реализацией синхронизированного блока кода.

синхронизированная реентерабельная блокировка

Когда я впервые услышал, что реентерабельная блокировка называется ReentrantLock, позже я узнал, что ключевое слово synchronized поддерживает неявный реентерабельность. Как следует из названия, блокировка с повторным входом — это блокировка, поддерживающая повторный вход и поддерживающая поток, который может многократно блокировать ресурсы. Для синхронизированного блока кода, когда другие потоки пытаются получить доступ к блоку кода, поток блокируется. Если поток, удерживающий блокировку, снова запросит удерживаемую им блокировку, он сможет успешно ее получить.

public synchronized void test1() {
    i++;
}

public void test2() {
    synchronized(this){
        test1();
    }
}

После того, как текущий поток получает блокировку, укажите _owner на текущий поток через cas. Если текущий поток снова запрашивает блокировку, точка _owner остается неизменной. Выполните _recursions++, чтобы записать количество повторных входов. Если попытка получить блокировку терпит неудачу, подождите в области _EntryList. Такое чувство немного похоже на сон во сне в Inception.Вы можете неоднократно входить в свой собственный сон.Если вы хотите нормально проснуться, вы можете только вернуться на исходный путь (_recursions--).

задним числом

В книге, которую я купил, нет низкоуровневого объяснения ключевого слова synchronized.Я могу только стоять на плечах других блоггеров в Интернете и иметь общее понимание принципа через объяснение лежащего в основе кода C++ в их статьях. .

И последнее, но не менее важное: конечная цель обучения — не интервью, интервью — это просто мотивация для мотивации обучения. Хватайтесь за вопросы интервью и получайте удовольствие от изучения новых знаний.

Ссылаться на:

Только немного Java -> анализ исходного кода jdk 2: структура памяти объекта, окончательный принцип синхронизации
"Глубокое понимание виртуальной машины Java"
«Искусство параллельного программирования»