Первоначально я писал о серии Mianba, и я написал этот вопрос, как я его написал:
Вы знаете принцип Synchronized?
Что касается Synchronized, я читал исходный код JVM HotSpot 1.8 в прошлом году для изучения волны.Тогда я нашел точку.Почти все статьи в Интернете, включая "The Art of Java Concurrent Programming", также говорили то же самое. точка.
В Интернете должно быть слишком много статей об эскалации блокировок.Здесь упоминается, что когда легковесный блокировщик CAS дает сбой, тоТекущий поток попытается использовать spin для получения блокировки..
На самом деле я сначала так и подумал, ведь так говорят, и это тоже очень разумно.
Поскольку тяжеловесная блокировка блокирует поток, если заблокированный код выполняется очень быстро, другие потоки не будут нуждаться в блокировке после прокрутки на некоторое время, и CAS может выполнить это напрямую, поэтому нет необходимости блокировать поток, а затем просыпаться. .
Но после того, как я посмотрел исходный код, я обнаружил, что это не так, этот код находится в synchronizer.cpp.
Итак, после сбоя CASнет операции отжима, в случае успеха CAS вернется напрямую, а в случае сбоя будет выполнен следующий метод раскрытия блокировки.
Я иду, чтобы заблокировать раздутый кодObjectSynchronizer::inflate
Я посмотрел и не увидел операции отжима.
Итак, из исходного кодаСбой CAS легкого замка не вращается, а напрямую переходит в тяжелый замок.
Однако для оптимизации производительности операция вращения существует в Synchronized.
который был повышен дотяжелый замокПосле этого, если поток не конкурирует за блокировку, он некоторое время будет вращаться и ждать освобождения блокировки.
Давайте все же посмотрим на исходный код, и одни только комментарии уже сделали это очень ясно:
В конце концов, накладные расходы на блокировку потоков для присоединения к очереди и последующего пробуждения все еще немного велики.
Посмотрим еще разTrySpin
операция, естьадаптивный спин, на самом деле, от фактического имени функцииTrySpin_VaryDuration
Можно отразить, что спин меняется.
На этом проблема Synchronized spin решена.Если тяжеловесная блокировка не выдержит конкуренцию, будет операция spin.Легкая блокировка этого действия не имеет (по крайней мере исходники 1.8 такие).Если кто-то опровергнет вы, пожалуйста, бросьте эту статью ему ха-ха.
Но сказав это, я просто продолжу говорить о Synchronized, в конце концов, скорость появления этой штуки все еще довольно высока.
Насколько глубока эта статья о Synchronized?
Позже, если интервьюер спросит, какой исходный код вы видели?
Прочитав эту статью, вы можете ответить: я видел исходный код JVM.
Конечно, исходный код немного больше,Я прошел все операции, связанные с Synchronized, все еще немного сложно.
Но читатели, которые уже читали мой анализ исходного кода, знают,Я нарисую блок-схему, чтобы организовать, так что даже если код не понятен, в процессе все равно можно разобраться!
Хорошо пойдем!
Начните с тяжелых замков
Synchronized был просто тяжеловесной блокировкой до 1.6.
Поскольку будет блокировка потока и пробуждение, эта операция реализуется системным вызовом операционной системы.Обычный Linux реализуется с использованием мьютекса pthread.
Я сделал скриншот исходного кода блокировки вызывающего потока, и вы можете видеть, что мьютекс действительно используется.
Когда дело доходит до системных вызовов, будет переключение контекста, то есть переключение между режимом пользователя и режимом ядра.Мы знаем, что накладные расходы на такое переключение довольно велики.
Так называемый тяжеловесный замок, и из-за этого будет упомянутая выше операция адаптивного вращения, потому что я не хочу заходить так далеко!
Рассмотрим принцип реализации тяжеловесных замков.
Ключевое слово Synchronized может украшать блоки кода, методы экземпляра и статические методы,существенно действует на объект.
Блок кода воздействует на объект внутри круглых скобок, метод экземпляра — это текущий объект экземпляра или this, а статический метод — текущий класс.
Есть такое понятиекритическая секция.
Мы знаем, что существует конкуренция из-за существования общих ресурсов, и несколько потоков хотят получить этот общий ресурс, поэтому область разделена, и код для работы с общими ресурсами ресурсов находится в этой области.
Можно понять, что если вы хотите войти в эту область, вы должны держать замок, иначе вы не можете войти.Эта область называется критической областью.
При оформлении блоков кода с помощью Synchronized
В это время в скомпилированном байт-коде будут инструкции monitorenter и monitorexit. Я привык понимать это с точки зрения критических секций. Enter — вход в критическую секцию, а exit — выход из критической секции, что соответствует получению блокировок и разблокировке .
На самом деле, эти две инструкции по-прежнему связаны с объектом, изменяющим блок кода, которым является код в приведенном выше коде.lockObject
.
Каждый объект имеет объект монитора, связанный с ним., поток, выполняющий команду monitorenter, пытается завладеть монитором, и если он захватывает его, он успешно получает блокировку.
Этот монитор будет подробно разобран ниже, давайте сначала посмотрим, как выглядит сгенерированный байт-код.
Сверху на картинке — байт-код, скомпилированный методом lockObject, а снизу — метод lockObject, который легче понять при взгляде на него.
Как видно из снимка экрана, monitorenter выполняется перед выполнением System.out Здесь выполняется действие блокировки блокировки, и после получения блокировки можно войти в критическую секцию.
После вызова следует инструкция monitorexit, указывающая, что блокировка снята и критическую секцию собираются закрыть.
Когда я также отметил на рисунке команду выхода из монитора,Из-за нештатных обстоятельств его также необходимо разблокировать., иначе он заблокируется.
Из сгенерированного байт-кода мы также можем узнать, почему синхронизированный не нужно разблокировать вручную?
Кто-то несет бремя за нас!Сгенерированный компилятором байткод делается за нас, а также рассматривается нештатная ситуация.
При оформлении метода с помощью синхронизированного
Байт-код, сгенерированный декорированным методом, отличается от декорированного блока кода, но по существу он такой же.
В настоящее время в байт-коде нет инструкций monitorenter и monitorexit, но втокен доступа для текущего методаСделал на нем трюк.
Я использую плагин idea для просмотра байт-кода, поэтому отображаемые буквальные результаты отличаются, но флаг тот же: 0x0021, который представляет собой комбинацию ACC_PUBLIC и ACC_SYNCHRONIZED.
Принцип заключается в том, чтобы пометить ACC_SYNCHRONIZED на флаге при изменении метода и отличить его по флагу ACC_SYNCHRONIZED в пуле констант времени выполнения, чтобы JVM знала, что этот метод помечен как синхронизированный, поэтому она будет выполнять операцию блокировки блокировки при входе метод. , то же самое может продолжать выполняться только в том случае, если блокировка получена.
Затем, будь то нормальный выход или аварийный выход, будет выполнена операция разблокировки, так что суть остается той же.
Также есть объект неявной блокировки, о котором я упоминал выше, модифицированный метод экземпляра — это этот, а модифицированный метод класса — текущий класс (про это есть яма, я написал этоэта статьяпроанализировано).
Я до сих пор помню, что был вопрос из интервью, кажется, его задавали, когда ByteDance работал,В чем разница на уровне байт-кода, когда интервьюер просит синхронизировать модифицировать методы и блоки кода?.
Как сказать? Неосознанно это ближе к битью байтов.
Давайте углубимся в синхронизацию
Из вышеизложенного мы уже знаем, что синхронизировано действует на объекты, но, не вдаваясь в подробности, далее разберем волну.
В Java структура объекта делится на заголовок объекта, данные экземпляра и заполнение выравнивания.
Заголовок объекта разбит на: MarkWord, указатель класса, длину массива (имеют только массивы), наш фокус на блокировках, поэтому фокус только на MarkWord.
Нарисую схему памяти MarkWord в разных состояниях, когда она 64-битная (монитор внутри неправильный, но менять не собираюсь, оставлю отметку ха-ха).
Причина, по которой структура MarkWord настолько сложна, заключается в том, что она должнасохранить память, чтобы одна и та же область памяти использовалась по-разному на разных этапах.
Помните эту картинку, все виды операций блокировки имеют сильную связь с этим MarkWord.
Как видно из рисунка, при использовании тяжеловесной блокировки флаг блокировки заголовка объекта равен 10, и будет указатель на объект монитора, поэтому объект блокировки и монитор связаны таким образом.
И этот монитор реализован на C++ в HotSpot и называется ObjectMonitor, который является реализацией монитора, также называемого монитором.
Выглядит это так, я аннотировал значения ключевых полей, а также вырезал комментарии заголовочного файла:
На мгновение вспомните, что исходный код тесно связан с этими полями.
Основной принцип синхронизации
Давайте сначала сфотографируем, совместив с комментариями монитора выше, сначала посмотрим, не беда, если вы в этом не разбираетесь, можно составить общее впечатление о потоке:
Хорошо, давайте двигаться дальше.
Ранее мы упоминали команду monitorenter, которая выполняет следующий код:
Сейчас мы анализируем тяжелые блокировки, поэтому нас не волнует предвзятый код, и скриншот в начале статьи о методе slow_enter как раз об этом, и в конечном итоге он будет выполнен дляObjectMonitor::enter
в этом методе.
Вы можете видеть, что смысл в том, чтобы установить _owner в ObjectMonitor для текущего потока через CAS,Если настройка выполнена успешно, это означает, что блокировка успешно получена..
Затем реентерабельность представляется самоинкрементом рекурсий.
В случае сбоя CAS выполняется следующий цикл:
На самом деле код EnterI уже был скриншотом выше, здесь я снова добавил важные операции постановки в очередь и удалил некоторые неважные коды:
Попробуйте сначала получить блокировку. Если это не сработает, будет самоадаптирующееся вращение. Если не сработает, упакуйте его как объект ObjectWaiter и добавьте в односвязный список _cxq. Существует также метод блокировки. .
Вы можете видеть, что независимо от того, какая ветвь будет выполненаSelf->_ParkEvent->park()
, это вызов, упомянутый вышеpthread_mutex_lock
.
Пока процесс скремблирования замков очень понятен, и я нарисую картинку, чтобы пояснить его.
Далее посмотрим, как разблокировать
ObjectMonitor::exit
Это метод, который будет вызываться при разблокировке.
Повторно входящие блокировки оцениваются на основе _recursions, повторный вход один раз _recursions++, разблокировка один раз _recursions--, если он уменьшен до 0, это означает, что блокировку необходимо снять.
потомВ этот момент разблокированный поток также разбудит поток, ожидавший ранее., здесь есть несколько режимов, давайте посмотрим.
еслиQMode == 2 && _cxq != NULL
когда:
еслиQMode == 3 && _cxq != NULL
В свое время я перехватил часть кода:
еслиQMode == 4 && _cxq != NULL
когда:
Если QMode не равен 2, он в конечном итоге выполнится:
На этом процесс разблокировки завершен! Нарисую еще одну блок-схему:
Далее рассмотрим метод вызова wait
Ничего особенного, просто добавьте текущий поток в двусвязный список _waitSet, а затем выполнитеObjectMonitor::exit
метод снятия блокировки.
Далее рассмотрим метод вызова notify
Ничего особенного, просто берем узел из головы _waitSet, а потом выбираем, ставить ли его в голову или хвост cxq или EntryList согласно стратегии, и просыпаемся.
Насчет notifyAll разбирать не буду, это тоже не что иное, как зациклиться и всех разбудить.
На данный момент несколько операций синхронизированы все живы, и когда вы выходите, вы можете сказать, что вы глубоко изучили синхронизацию.
Теперь взгляните на эту картинку еще раз, вы должны многое иметь в виду.
Почему есть два списка _cxq и _EntryList для размещения потоков?
Так как одновременно будет несколько потоков, конкурирующих за блокировки, односвязный список _cxq создается на основе CAS для поддержки этого параллелизма, а затем создается двусвязный список _EntryList для перемещения некоторых узлов потока каждый раз, когда он просыпается, чтобы уменьшить конкуренцию за хвост для _cxq.
Ввести вращение
Принцип синхронизирования должен быть в целом ясен, а еще мы знаем, что нижний слой будет использовать системные вызовы, которые будут иметь большие накладные расходы, так что подумайте, как его оптимизировать?
Я это уже понял из подзаголовка План спиннинг Об этом было сказано в начале статьи, упомяну здесь.
Вращение на самом деле простаивает ЦП, выполняя какие-то бессмысленные инструкции,Цель состоит в том, чтобы не дать ЦП ждать снятия блокировки..
В обычных условиях, если получение блокировки не удается, очередь должна быть заблокирована, но иногда, как только блокировка блокируется, другие потоки освобождают блокировку, а затем пробуждают только что заблокированный поток, что необязательно.
Поэтому, когда конкуренция потоков не очень жесткая, покрутите некоторое время, и может быть возможно получить блокировку напрямую, не блокируя поток, что позволяет избежать ненужных накладных расходов и повысить производительность блокировки.
ноКоличество вращений - еще одна трудность, в ситуации с высокой конкуренцией прокрутка — это пустая трата ЦП, потому что в результате прокрутка через некоторое время заблокируется.
Поэтому в Java реализован адаптивный спин, который динамически регулирует количество спинов в соответствии с количеством предыдущих спинов.Это называется делать что-то на основе исторического опыта.
Обратите внимание, что это шаг тяжелого замка, не забывайте, что я сказал в начале статьи~.
До сих пор принцип синхронного тяжеловесного замка должен быть предельно ясен?
Нижний уровень синхронизации реализуется с помощью объектов монитора, CAS и мьютексов, мьютекс-блокировок.Внутри будут очереди ожидания (cxq и EntryList) и очереди ожидания состояния (waitSet) для хранения соответствующих заблокированных потоков.
Потоки, которые не конкурируют за блокировки, сохраняются в очереди ожидания, а потоки, которые получают блокировки, сохраняются в условной очереди ожидания после вызова ожидания.Разблокировка и уведомление разбудят ожидающие потоки в соответствующей очереди, чтобы конкурировать за блокировку.
Затем, поскольку блокировка и пробуждение зависят от базовой реализации операционной системы, в системных вызовах происходит переключение между режимом пользователя и режимом ядра, что приводит к высоким накладным расходам, поэтому такая блокировка называется усиленной блокировкой.
Поэтому вводится адаптивный механизм вращения для улучшения работы замка.
Теперь пришло время ввести облегченные замки
Давайте еще раз подумаем, есть ли такой сценарий:Несколько потоков запрашивают одну и ту же блокировку в разные периоды времени., в настоящее время вообще нет необходимости блокировать потоки, даже отслеживать объекты, поэтому вводится концепция облегченных блокировок, позволяющая избежать системных вызовов и сократить накладные расходы.
В отсутствие интенсивной конкуренции замков этот сценарий все еще очень распространен и может быть нормой, поэтому необходимо введение облегченных замков.
Прежде чем представить принцип облегченной блокировки, давайте взглянем на предыдущую диаграмму MarkWord.
Облегченная блокировка работает с MarkWord заголовка объекта.
Если установлено, что в настоящее время он находится в состоянии без блокировки, область с именем LockRecord будет нарисована в текущем кадре стека текущего стека потока, а затем копия MarkWord объекта блокировки будет скопирована в LockRecord. и вызывается dhw (то есть выполняется метод set_displaced_header) внутри.
Затем укажите заголовок объекта блокировки на этот LockRecord через CAS.
Процесс запирания облегченного замка:
Если в настоящее время он находится в заблокированном состоянии и удерживается текущим потоком, поместите null в dhw, что является логикой повторных блокировок.
Рассмотрим логику облегченной разблокировки замка:
Логика по-прежнему очень проста, то есть через CAS изменить метку (dhw), хранящуюся в LockRecord в текущем кадре стека, обратно на заголовок объекта.
Если полученный dhw равен нулю, это означает, что он реентерабельный в это время, поэтому вы можете просто вернуть его напрямую, в противном случае это использовать CAS для обмена, если CAS не работает, это означает, что в это время есть конкуренция, тогда надуйте!
Позвольте мне сказать еще несколько слов об этом легком замке.
Каждая блокировка должна быть в вызове метода, а вызов метода — это кадр стека, помещенный в стек.Если это облегченная реентерабельная блокировка, то dhw в кадре стека, помещенном в стек в это время, равно нулю, в противном случае — объект блокировки.
Таким образом, при разблокировке значение dhw можно использовать для определения того, является ли он повторным в данный момент.
Теперь, чтобы ввести предвзятую блокировку
Давайте еще раз подумаем, есть ли такой сценарий: в начале блокировку держит только один поток, и других потоков для конкуренции не будет.В это время частый CAS не нужен, а у CAS тоже есть накладные расходы.
Поэтому исследователи JVM задействовали смещенную блокировку, то есть смещенную в сторону потока, тогда этот поток может напрямую получить блокировку.
Смотрим еще раз на картинку, а смещение заперто во втором ряду.
Принцип несложный: если текущий объект блокировки поддерживает предвзятые блокировки, он будет работать через CAS: запишите адрес текущего потока (он же используется как уникальный идентификатор) в markword, а последние три бита поля mark установите в 101.
После того, как поток запрашивает эту блокировку, необходимо только определить, равны ли последние три цифры маркворда 101 и указывает ли он на адрес текущего потока.
Есть еще один момент, который может быть упущен многими статьями, то есть необходимо судить, совпадает ли значение эпохи со значением объекта блокировки.ДобрыйЗначения эпох в совпадают.
Если все удовлетворены, это означает, что текущий поток удерживает смещенную блокировку и может вернуться напрямую.
Для чего эта эпоха?
Это можно понимать как несколько поколений несимметричных замков.
Смещенная блокировка должна выполнять операцию отзыва, когда есть конкуренция, фактически, она должна быть обновлена до облегченной блокировки.
Когда объект класса отзывается слишком много раз, например, объект класса Yes используется как предвзятая блокировка, которая часто отзывается Когда количество раз достигает определенного порога (XX: BiasedLockingBulkRebiasThreshold, по умолчанию 20 ), современная смещенная блокировка будет отброшена, и класс будет отброшен epoch плюс один.
Поэтому, когда значения эпохи объекта класса и объекта блокировки не равны, текущий поток может повторно сместить блокировку на себя, потому что предыдущее поколение смещенных блокировок было заброшено.
ноЧтобы гарантировать, что выполняющийся поток, удерживающий блокировку, не может потерять блокировку из-за этого, предвзятая отмена блокировки требует, чтобы все потоки находились в безопасной точке, а затем проходит через стеки Java всех потоков,Найдите заблокированный экземпляр классаи увеличьте значение эпохи в их поле тега на 1.
Когда количество отзывов превышает другое пороговое значение (XX:BiasedLockingBulkRevokeThreshold, значение по умолчанию — 40), функция смещения этого класса отбрасывается, что означает, что этот класс не может быть смещен.
На этом этапе весь синхронизированный процесс должен быть относительно ясен.
Я говорю о процессе эскалации блокировок в обратном порядке, потому что на самом деле сначала идут тяжеловесные блокировки, а затем по фактическому анализу оптимизируются необъективные блокировки и облегченные блокировки.
Включение некоторых деталей периода также должно быть более понятным, я думаю, что это почти то же самое для Synchronized.
Я сделал еще одну картинку на вики openjdk, чтобы убедиться, что она очень понятна:
В конце концов
Причина анализа исходного кода в том, что я читаю информацию, но многие детали не ясны, и тогда я чувствую себя очень некомфортно, поэтому у меня нет другого выбора, кроме как стиснуть зубы.
Для кого-то вроде меня, кто в основном не силен в С++, это действительно немного сложно... Я писал это в течение недели.
На самом деле, я не планировал так много писать, я просто хотел написать часть о вращении... Я не мог остановиться, когда делал это.
а также,Если есть какие-либо ошибки, пожалуйста, немедленно свяжитесь со мной.
В этой статье много кода, я не знаю, сколько людей смогут терпеливо его увидеть...
Я думаю, что здесь каждый мастер! Можешь вычесть 1 и показать мне?
плечи гигантов
«Углубленный разбор виртуальной машины Java» Чжэн Юди
wiki.open JDK.Java.net/display/hot…
docs.Oracle.com/JavaColor/spec…
Поиск в Wechat [стратегия прокачки да] больше статей ждут вас, чтобы прочитать, мое резюме статьи:GitHub.com/также Смекта/имеет…Добро пожаловать, звезда!
Я да, от немного до миллиона точек, добро пожаловать смотреть, переслать, оставить сообщение, увидимся в следующей статье.