Анализ базовой реализации и процесса укрупнения блокировки синхронизированного

Java
Анализ базовой реализации и процесса укрупнения блокировки синхронизированного

В Яве,synchronizedКлючевые слова используются для управления синхронизацией потоков. То есть в многопоточной среде управлениеsynchronizedСегменты кода не выполняются несколькими потоками одновременно.

Так как же synchronized обеспечивает синхронизацию потоков? И каков процесс эскалации блокировки? Давайте исследовать.

0x01 сведения о синхронизированной реализации

1.1 Реализация кода Java

Давайте сначала посмотрим, что произойдет, если несколько потоков будут конкурировать за общие ресурсы и не будут принимать меры:

public class TestSync implements Runnable {

    private int count = 100;

    public static void main(String[] args) {
        TestSync ts = new TestSync();
        Thread t1 = new Thread(ts, "线程1");
        Thread t2 = new Thread(ts, "线程2");
        Thread t3 = new Thread(ts, "线程3");

        t1.start();
        t2.start();
        t3.start();
    }

    @Override
    public void run() {
        while (true) {
            if (count > 0) {
                count--;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
            } else {
                break;
            }
        }
    }
}

Поток 2 уменьшает счет до 97. Поток 3 и поток 1 также выполняют подсчет -- в определенный момент, но результат также равен 97, что указывает на то, что они не знают, что другие потоки также выполняют подсчет, когда они подсчитывают --.

Эту проблему, я думаю, все знают, что ее можно решить, добавив синхронизацию.

Измените метод запуска следующим образом:

@Override
public void run() {
    while (true) {
        synchronized (this) {
            if (count > 0) {
                count--;
                System.out.println(Thread.currentThread().getName() + " count = " + count);
            } else {
                break;
            }
        }
    }
}

Выполняйте счет -- методично, без неуверенности.

Поэтому на уровне кода добавляйте ключевые словаsynchronizedМожет решить вышеуказанную проблему безопасности потоков.

1.2 Как реализовать синхронизацию на уровне байт-кода

Если вы используете IDEA, рекомендуется установить его здесьjclasslib Bytecode viewer, этот плагин может легко увидеть инструкции по выполнению байт-кода программы:

Давайте взглянем на инструкции байт-кода программы прямо сейчас:

ФактическиsynchronizedНа уровне байт-кода реализацияmonitorenterиmonitorexitинструкция, эти два могут быть синхронизированы.

"monitorenter":

Объект Java по своей сути является монитором.Когда монитор занят, он находится в заблокированном состоянии.

Каждый объект связан с монитором. А монитор блокируется только если его нить держат.

воплощать в жизньmonitorenterПоток пытается завладеть монитором:

  • Если монитор, связанный с objectref, имеет счетчик входов, равный 0, поток входит в монитор и устанавливает его счетчик входов равным 1. Тогда поток становится владельцем монитора.
  • Если поток уже владеет монитором, связанным с objectref, он повторно войдет в монитор, увеличив количество входов. Это повторный вход в блокировку.
  • Если другой поток уже владеет монитором, связанным с objectref, поток будет блокироваться до тех пор, пока счетчик записей монитора не станет равным нулю, а затем снова попытается стать владельцем.

"monitorexit":

один или большеMonitorExitИнструкции можно комбинировать сMonitorenterДирективы используются вместе, и вместе они реализуют синхронизированный оператор.

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

JVM завершает работуmonitorexitМетод обработки делится на обычный выход и выход при возникновении исключения:

  • Выход монитора после завершения обычного синхронизированного метода обрабатывается инструкцией return виртуальной машины Java. То есть, когда программа выполняется нормально, JVM имеет инструкцию, которая неявно завершает выход из монитора ---monitorexit, это инструкцияathrow.
  • Если в операторе синхронизации есть исключение, механизм обработки исключений JVM также можетmonitorexit.
简单的加锁解锁过程
Простой процесс блокировки и разблокировки

Итак, после выполнения блока синхронизированного кода сначала выполнитеmonitorenterкоманда, при выходеmonitorexitинструкция.

1.3 Реализация уровня JVM

public static void main(String[] args) {
    Object o = new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());

    synchronized (o) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

Результаты:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           08 f3 7f 02 (00001000 11110011 01111111 00000010) (41939720)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

Если синхронизация не добавлена, значение информации заголовка объекта равно01 00 00 00, после добавления блокировки изменился заголовок объекта08 f3 7f 02, указывая на то, что synchronized изменит новую информацию заголовка объекта. Заголовок объекта вызывается в Hotspotmarkword.

объектmarkwordВ нем очень важная информация, самая важная из которых - замокsynchronized. (Существует также информация GC в markword, а также информация о хэш-коде.)

"Информация о ключевом слове JVM, реализованном Hotspot на 64-битной машине.":

markword信息
информация о ключевом слове

0x02 Процесс обновления блокировки

2.1 Процесс обновления

На заре JDK базовая реализация synchronized была тяжеловесной, так называемой тяжеловесной, которая обращалась непосредственно в операционную систему, чтобы запросить блокировку, и ее эффективность была очень низкой.

Позже JDK оптимизировал синхронизированную блокировку, так что锁升级Концепция чего-либо.

锁升级Процесс примерно такой:

new -> "Блокировка смещения" -> "Облегченные блокировки (спинлоки)"-> "тяжелый замок"

Процесс синхронизированной оптимизации тесно связан с markword.

Три младших бита в меточном слове используются для представления состояния блокировки, из которых 1 бит является смещенным битом блокировки, а последние два бита являются нормальным битом блокировки.

  1. Object o = new Object()

замок = 0 01 без замка состояния

Примечание. Если блокировка смещения включена, по умолчанию используется состояние анонимного смещения.

  1. o.hashCode()

001 + hashcode

  1. Синхронизировано по умолчанию (o)

00 -> Легкий замок

По умолчанию блокировка смещения имеет задержку, по умолчанию 4 секунды.

Почему?Потому что в самой виртуальной машине JVM есть некоторые потоки, которые запускаются по умолчанию, и в ней много кодов синхронизации.Когда эти коды синхронизации запускаются, они знают, что будет конкуренция.Если используются предвзятые блокировки, предвзятые блокировки будет постоянно отзываться и обновляться, эффективность низкая.

Вы можете использовать параметр BiasedLockingStartupDelay, чтобы указать, следует ли запускать блокировку смещения (=0, запускать блокировку смещения немедленно):

-XX:BiasedLockingStartupDelay=0
  1. Если блокировка смещения активирована

Процесс обновления блокировки: новый объект () -> 101 Biased Lock -> Thread ID равен 0 -> Anonymous BiasedLock

Откройте блокировку смещения, новый объект по умолчанию является смещенным анонимным объектом 101.

  1. Если поток заблокирован

Блокировка со смещением вверх относится к процессу изменения идентификатора потока маркерного слова на собственный идентификатор потока.

Блокировки смещения не могут быть перенастроены, смещены в пакетном режиме и отозваны в пакетном режиме.

  1. Если есть конфликт тем

Отменить предвзятую блокировку и обновить ее до облегченной блокировки.

Поток генерирует LockRecord в собственном стеке потоков и использует операцию CAS для установки маркерного слова в качестве указателя на LR своего собственного потока, и успешный поток получает блокировку.

  1. Если конкуренция усилится

Конкуренция обостряется: есть потоки, которые крутятся более 10 раз (параметр -XX:PreBlockSpin можно настроить), или количество крутящихся потоков превышает половину числа ядер ЦП, после JDK 1.6 добавляется Adaptive Self Spinning и JVM управляет им сама.

Обновите тяжелые блокировки: подайте заявку на ресурсы в операционной системе, linux mutex, вызовы ЦП из системы уровня 3-0, поток зависает, входит в очередь ожидания, ожидает планирования операционной системы, а затем отображает обратно в пространство пользователя .

Подводя итог, процесс эскалации блокировки, вероятно, выглядит так:

锁升级过程
процесс укрупнения блокировки

2.2 Зачем нужны тяжелые блокировки, когда у нас есть спин-блокировки?

Вращение потребляет ресурсы ЦП.Если время блокировки велико или много вращающихся потоков, ЦП будет потребляться много.

Для тяжеловесных блокировок существует очередь ожидания, все те, кто не может получить блокировку, попадают в очередь ожидания без потребления ресурсов процессора.

2.3 Обязательно ли предвзятые блокировки более эффективны, чем спин-блокировки?

Не обязательно, в случае, когда четко известно, что будет многопоточная конкуренция, предвзятая блокировка обязательно будет включать отзыв блокировки, в этом случае спин-блокировка используется напрямую.

В процессе запуска JVM будет большая конкуренция потоков (очевидно), поэтому по умолчанию блокировка смещения не открывается при запуске и будет открыта через некоторое время.

2.4 Нижняя реализация синхронизированного

На аппаратном уровне блокировка фактически выполняетсяlock cmpxchg xxинструкция.

синхронизировано на уровне байт-кода:

Если блокировка является методом, jvm добавит синхронизированный модификатор;

Если код синхронизации быстрый, используйте инструкции monitorenter и monitorexit.

когда JVM видитsynchronized修饰符илиmonitorenter和monitorexitКогда соответствующий C++ вызывает механизм синхронизации, предоставляемый операционной системой.

Уровень ЦП используетсяlockинструкции реализованы.

Например, мы хотим установить число i в определенном блоке синхронизированной памяти и изменить значение i с 0 на 1. Этот процесс может иметь несколько инструкций или не может быть синхронизирован (слишком быстро) при выполнении на ЦП, поэтому нам нужна инструкция блокировки.

Если перед cmpxchg добавить блокировку, эта область будет заблокирована во время выполнения последующих инструкций.Только эта инструкция может быть изменена, а другие инструкции не могут быть выполнены.

0x03 Сводка

  • Заголовок объекта Java"markword"

    В виртуальной машине Hotspot расположение объектов в памяти разделено на три области:

    • заголовок объекта
    • данные экземпляра
    • Выровнять отступы

Заголовок объекта Java является основой для реализации синхронизированных объектов блокировки.Вообще говоря, объекты блокировки, используемые синхронизацией, хранятся в заголовке объекта Java. Это ключ к облегченным замкам и замкам со смещением.

  • "monitor"

Инструмент синхронизации также может быть описан как механизм синхронизации.

Почему каждый объект может быть замком? Поскольку каждый объект Java имеет собственный объект C++ oop/oopDesc ​​внутри JVM, а соответствующий oop/oopDesc ​​будет иметь заголовок объекта markOop, и этот заголовок объекта является местом, где хранится блокировка, а также мониторинг объекта Это одна из причин, по которой каждый объект может быть замком.

  • "Блокировка процесса эскалации (оптимизации)"

Синхронизированная блокировка оптимизирована, введены смещенные блокировки и облегченные блокировки; уровень блокировок постепенно повышается с низкого до высокого, без блокировок -> смещенные блокировки -> облегченные блокировки -> тяжелые блокировки.

  • "Блокировка смещения"

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

На:"-XX:BiasedLockingStartupDelay=0"

  • "блокировка спина"

Существует ограничение на время или количество ожиданий вращения.Если вращение превышает определенное время и все еще не получает блокировку, поток должен быть приостановлен.

В JDK1.6 включен -XX:+UseSpinning, -XX:PreBlockSpin=10 — количество спинов, после JDK1.7 этот параметр убран и контролируется jvm.

  • "тяжелый замок"

Тяжеловесная блокировка реализуется монитором внутри объекта. Суть монитора в том, чтобы полагаться на реализацию Mutex Lock базовой операционной системы. Переключение между потоками операционной системы требует переключения из пользовательского режима в режим ядра, а стоимость переключения очень высока.


Первый публичный аккаунт статьи:"линейный барьер", добро пожаловать, чтобы обратить внимание на исправления чтения. Связанный код был синхронизирован с"GitHub" GitHub.com/Ответственность маленького белого дракона/Java….