Что за существование замок?

Java
Что за существование замок?

С развитием бизнеса и увеличением числа пользователей проблемы с высоким параллелизмом часто становятся очень сложной проблемой, с которой приходится сталкиваться и с которой приходится сталкиваться программистам, а параллельное программирование является относительно продвинутым и малопонятным знанием в области программирования. хорошо изучить параллелизм При наличии знаний написание хороших параллельных программ не так просто. Программисты, пишущие на Java, на данный момент могут быть относительно счастливы, потому что существует большое количество инкапсулированных примитивов синхронизации и классов инструментов синхронизации, написанных мастерами на Java, что снижает порог для написания правильных и эффективных параллельных программ. Хотя такой высокий уровень абстракции инкапсуляции упрощает написание программ, он мешает нам понять его внутренний механизм реализации.Теперь проведем аналогию с точки зрения блокировок в реальном мире и посмотрим, какие блокировки бывают в программном мире. существования это?

Блокировки в процедурном мире

Если кто-то спросит вас: «Как сделать так, чтобы в дом не заходили посторонние»? Я думаю, вы можете легко подумать: «Заприте его!». А если кто-то спросит вас: «Как бороться с параллелизмом нескольких потоков»? Я думаю, вы можете ляпнуть: «Заблокируйте его!». Подобные сценарии легко понять в реальном мире, но в процедурном мире эти слова полны путаницы. Мы видели все виды блокировок в реальном мире, так как же выглядят блокировки в Java? В нашем реальном мире нам обычно нужен ключ, чтобы открыть замок, чтобы войти в дом.Какой ключ открывает замок в программном мире? В действительности замок обычно находится на двери или на шкафу или в других местах.Где находится замок в программном мире?В реальном мире обычно мы запираем и отпираем замок, так кто же является замком и разблокировать в мире программ?

Ответив на эти вопросы, мы хотим получить более глубокое представление о том, какие существуют блокировки существования в Java? С чего начать разбираться, я думаю в программе в первую очередь используются блокировки, так что приступим к исследованию использования блокировок!

использование замков

Что касается блокировок в Java, их обычно можно разделить на две категории: одна — это примитив синхронизации Synchronized, предоставляемый на уровне JVM, а другая — это классы реализации интерфейса Lock на уровне Java API. Блокировки на уровне Java API, такие как Reentrantlock и ReentrantReadWriteLock, имеют очень подробные исходные коды. Вы можете пойти и посмотреть, как они реализованы. Возможно, вы найдете ответы выше. Давайте взглянем на Synchronized.

Сначала посмотрите на следующий код:

public class LockTest
{
    Object obj=new Object();
    public static synchronized void testMethod1()
    {
        //同步代码。
    }
    public synchronized void testMethod2()
    {
        //同步代码
    }
    public void testMethod3()
    {
        synchronized (obj)
        {
            //同步代码
        }
    }
}

Многие книги по параллельному программированию резюмируют использование Synchronized следующим образом:

  • Когда Synchronized изменяет статический метод (соответствующий testMethod1), блокировка является объектом класса текущего класса, который соответствует LockTest.class.объект.
  • Когда Synchronized изменяет метод экземпляра (соответствующий testMethod2), объект текущего экземпляра класса блокируется, что соответствует ссылке this в LocKTest.объект.
  • Когда Synchronized изменяет блок синхронизированного кода (соответствующий testMethod3), экземпляр объекта в круглых скобках блока синхронизированного кода блокируется, что соответствует здесь objобъект.

Отсюда мы видим, что использование Synchronized зависит от конкретных объектов,Отсюда можно обнаружить, что между блокировками и объектами существует некоторая связь.. Итак, давайте взглянем на следующий шаг, чтобы увидеть, какие подсказки о замке содержатся в объекте.

композиция объектов

Все в Java является объектом, точно так же, как у вашего объекта длинные волосы и большие глаза (возможно, это все просто воображение)... Объекты в Java состоят из трех частей. Это заголовок объекта, данные экземпляра и заполнение выравнивания.

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

对象组成.png

Давайте посмотрим на содержимое заголовка объекта:

длина содержание иллюстрировать
32/64bit Mark Word Сохраните HashCode или информацию о блокировке объекта
32/64bit Class Metadata Address указатель на данные типа объекта
32/64bit Array length длина массива (если текущий объект является массивом)

Возьмем для примера 32-битную виртуальную машину (достаточно 64-битной аналогии), МаркВорд имеет всего четыре байта, а также хранит такую ​​информацию, как HashCode.Возможно ли, что блокировка существует полностью в этих четырех байтах? Это предложение совершенно неверно до Jdk1.6 и верно в некоторых случаях после Jdk1.6.

Почему ты это сказал?

Это связано с тем, что потоки в Java соответствуют один к одному с локальными потоками операционной системы, а операционная система делит системное пространство на пользовательский режим, чтобы защитить внутреннюю безопасность системы, предотвратить случайные вызовы некоторых внутренних инструкций, и т. д., а также обеспечить безопасность ядра. В отличие от режима ядра, потоки, которые мы обычно запускаем, выполняются только в пользовательском режиме. Когда нам нужно вызывать службы операционной системы (называемые здесь системными вызовами), такие как чтение, запись и другие операции, нет возможности напрямую инициировать вызовы в пользовательском режиме.В настоящее время необходимо переключаться между пользовательским режимом и режимом ядра. Причина, по которой Synchronized раньше называлась тяжеловесной блокировкой, заключалась в том, что блокировка и разблокировка Synchronized требует переключения между пользовательским режимом и режимом ядра, поэтому ранняя Synchronized — это тяжеловесная блокировка, которая должна блокировать и пробуждать потоки. и enqueue блокирующей очереди и условной очереди и т.д., о которых мы поговорим позже, заведомо невозможно хранить в этих четырех байтах. Но Jdk1.6 сделал ряд оптимизаций для Synchronized, включая укрупнение блокировки, что сделало это предложение частично верным.

Процесс эскалации блокировки

Причина, по которой предыдущее предложение в некоторых случаях верно, заключается в том, что в Jdk1.6 команда виртуальной машины провела ряд оптимизаций для Synchronized, мы не будем обсуждать специфику, во многих книгах по параллельному программированию есть подробные записи. И здесь мы хотим поговорить об одной из важных оптимизаций — эскалации блокировок.

Процесс эскалации блокировки Synchronized в Java выглядит следующим образом: нет блокировки -> предвзятая блокировка -> облегченная блокировка -> усиленный мьютекс.

То есть, если между несколькими потоками нет серьезной конкуренции за блокировки, Synchronized не будет использовать тяжелые блокировки мьютекса до Jdk1.6.

Мы знаем, что в реальном мире мы отвечаем за блокировку и разблокировку, поэтому в мире программ потоки играют роль людей, которых нужно блокировать и разблокировать.

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

В начале, в разблокированном состоянии, мы можем понять, что дверь сокровищницы не заперта, в это время первый поток бежит в область кода синхронизации (первый человек идет к двери), и блокировка смещения Добавлен замок какой формы на данный момент? На данный момент это фактически похоже на блокировку распознавания лиц.Первый поток, который входит в сам блок кода синхронизации, используется в качестве ключа, а идентификатор потока, который может однозначно идентифицировать поток, сохраняется в Mark Word.

Содержание в Mark Word на данный момент следующее:

偏向锁.jpg

23 бита из четырех байтов здесь используются для хранения идентификатора потока первого потока, который получает предвзятую блокировку, 2-битная эпоха представляет действительность предвзятой блокировки, 4-битный возраст генерации объекта и 1-бит. бит, является ли это смещенной блокировкой (1 Да), 2-битный флаг блокировки (01 — смещенная блокировка).

Когда первый поток запускается к блоку синхронизированного кода, он проверяет заголовок объекта, используемого блокировкой Synchronized.Если идентификатор потока заголовка объекта одного из трех объектов, используемых Synchronized, упомянутых выше, равен Если он пуст. , и блокировка смещения действительна, это означает, что он все еще находится в свободном от блокировки состоянии (то есть сокровищница не была заблокирована), то в это время первый поток будет использовать CAS для замены своего идентификатора потока на заголовок объекта. Идентификатор потока Mark Word. Если замена прошла успешно, это означает, что поток получил блокировку смещения, после чего поток может безопасно выполнить код синхронизации. Если поток снова введет код синхронизации в будущем, если другие потоки не получают блокировку смещения в течение этого периода, вам нужно просто сравнить, соответствует ли ваш идентификатор потока идентификатору потока в Mark Word.Если они согласуются, вы можете напрямую войти в область кода синхронизации, чтобы потери производительности намного меньше.

Предвзятые блокировки основаны на том факте, что команда R&D компании HotSpot однажды провела исследование и показала, что при нормальных обстоятельствах блокировки не конкурируют друг с другом, и блокировки всегда запрашиваются одним и тем же потоком несколько раз. чтобы иметь большие преимущества!

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

Легкий замок

Отсюда мы можем видеть, что когда блокировка начинается с предвзятой блокировки, какая форма существует Мы также говорили ранее, что предвзятые блокировки существуют без нескольких потоков, конкурирующих за блокировки, но высокий параллелизм. Конкурентные блокировки неизбежны в среде, и в на этот раз Synchronized начал свое продвижение.

Когда есть несколько потоков, конкурирующих за блокировки, простая смещенная блокировка в это время не так безопасна, и блокировка не может быть заблокирована, В это время блокировку следует изменить и обновить до более надежной блокировки. Процесс обновления блокировки в настоящее время можно условно разделить на два этапа: (1) отзыв предвзятых замков (2) обновление облегченных замков.

Прежде всего, как отозвать предвзятую блокировку? Мы говорим, что предвзятая блокировка на самом деле является идентификатором потока в Mark Work. В настоящее время, пока слово метки изменяется, это, естественно, эквивалентно отзыву предвзятой блокировки. Затем проблема в том, что предвзятая блокировка представлена ​​идентификатором потока, который легковесен. Что означает блокировка? Ответ — Lock Record (запись блокировки в кадре стека).

Здесь я объясню:

Мы знаем, что структуру памяти JVM можно разделить на (1) кучу (2) стек виртуальной машины (3) локальный стек методов (4) счетчик программ (5) область методов (6) прямую память. Среди них счетчик программ и стек виртуальной машины являются частными для потока.Каждый поток имеет свое собственное независимое пространство стека.Похоже, что его можно хранить в стеке, чтобы различать, какой поток получил блокировку.На самом деле JVM делает Вот что он делает.

Во-первых, JVM откроет часть памяти в текущем стеке, которая называется записью блокировки, и скопирует содержимое слова метки в запись блокировки (то есть в записи блокировки хранится предыдущая запись). 1) Контент в Mark Work, зачем сохранять предыдущий контент? Это очень просто, ведь мы собираемся модифицировать контент Mark Word, конечно же, сохранить его перед модификацией, чтобы его можно было восстановить в будущем), после копирования, следующий шаг — я начал модифицировать Mark Word, как его модифицировать? Конечно, это замена Mark Word на CAS! На этом этапе Mark Word станет следующим:

轻量级锁.jpg

Вы можете видеть, что Mark Word использует 30 бит для записи записи блокировки, которую мы только что создали в кадре стека.Флаг блокировки равен 00, чтобы указать облегченную блокировку, поэтому легко узнать, какой поток получил облегченную блокировку.

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

Очевидно, облегченные блокировки подходят для ситуаций, когда конкуренция замков не является жесткой и блокировка удерживается в течение короткого времени, наоборот, если конкуренция замков является жесткой или поток долго не освобождает блокировку после получения блокировки. lock, поток будет крутиться напрасно (бесконечный цикл) и тратить ресурсы процессора.

Тяжеловесный мьютекс

Когда слишком много желающих войти в сокровищницу, легкости недостаточно, в это время мы можем использовать только убийцу — тяжеловесный мьютекс. Это также реализация Synchronized по умолчанию до Jdk1.6.

Когда блокировка находится в легковесной блокировке, поток должен вращаться и ждать, пока поток, удерживающий блокировку, освободит блокировку, а затем применить блокировку, но есть две проблемы:

  1. Есть много вращающихся потоков, то есть многие потоки ждут, пока поток, удерживающий блокировку в данный момент, освободит блокировку. потоки не могут получить блокировку. Не может все время вращаться, верно?
  2. Поток, удерживающий блокировку, не освобождает блокировку в течение длительного времени, так что поток, ожидающий снаружи, чтобы получить блокировку, не может получить блокировку после длительного вращения.

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

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

Сложность тяжеловесных блокировок самая высокая.Поскольку поток, удерживающий блокировку, должен разбудить заблокированный ожидающий поток при освобождении блокировки, когда поток не может получить блокировку, он должен войти в определенную область блокировки, заблокировать и ожидать равномерно. При этом мы знаем, что есть ожидание, ожидание и пробуждение условия уведомления нужно обрабатывать, поэтому для реализации тяжеловесных блокировок требуется дополнительный большой киллер — Монитор.

Это описано в книге «Искусство параллельного программирования на Java»:

JVM реализует синхронизацию методов и синхронизацию блоков кода на основе входа и выхода из объекта Monitor, но детали их реализации различаются. Синхронизация блоков кода реализована с помощью инструкций monitorenter и monitorexit, а синхронизация методов реализована другим способом, подробностей нет в спецификации JVM Подробное описание. Однако с помощью этих двух инструкций также можно добиться синхронизации методов.

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

В качестве примера возьмем виртуальную машину HotSpot, она реализована на C++, а C++ также является объектно-ориентированным языком, поэтому на этот раз команда разработчиков виртуальной машины решила представить замок в виде объекта. время, C++ также поддерживает полиморфизм.Здесь монитор На самом деле это абстракция.Реализация монитора в виртуальной машине использует ObjectMonitor.Взаимоотношение между монитором и ObjectMonitor можно сравнить с отношением между Map и HashMap в Java.

Давайте взглянем на истинное содержимое ObjectMonitor:

  ObjectMonitor() 
  {
    _header       = NULL;
    _count        = 0;//用来记录该线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0;//锁的重入次数
    _object       = NULL;
    _owner        = NULL;//指向持有ObjectMonitor的线程
    _WaitSet      = NULL;//存放处于Wait状态的线程的集合
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;//所以等待获取锁而被阻塞的线程的集合
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

Настоятельно рекомендуется взглянуть на исходный код реализации ReentrantLock на основе AQS (Abstract Queue Synchronizer), поскольку идея реализации синхронизации внутри ReentrantLock, по сути, является воплощением реализации Monitor in Synchronized.

Прежде всего, ObjectMonitor должен иметь указатель на поток, который в данный момент получает блокировку, которая является указанным выше владельцем.Когда поток получает блокировку, он вызывает метод ObjectMonitor.enter() для входа в блок кода синхронизации. при получении блокировки будет установлен владелец. Чтобы указать на текущий поток, когда другие потоки попытаются получить блокировку, они найдут владельца в ObjectMonitor, чтобы убедиться, что это они сами. Если это так, рекурсии и подсчет будут увеличивается на 1, что означает, что поток снова получил блокировку (доступна Synchronized). Блокировка повторного входа, поток, удерживающий блокировку, может снова получить блокировку), иначе он должен быть заблокирован, так где же эти заблокированные потоки? Его можно разместить в EntryList единообразно. Когда поток, удерживающий блокировку, вызывает метод ожидания (мы знаем, что метод ожидания заставит поток отказаться от процессора, снять удерживаемую им блокировку, а затем заблокировать и приостановить себя до тех пор, пока другие потоки не вызовут метод notify или notifyAll), тогда блокировка потока должна быть снята, владелец должен быть установлен в нуль, а поток, заблокированный в ожидании получения блокировки в EntryList, должен быть пробужден, а затем приостановить себя и войти в коллекцию waitSet для ожидания.Когда другие потоки удерживают блокировку вызовите метод notify или notifyAll. Определенный поток (notify) или все потоки (notifyAll) в WaitSet будут перемещены из WaitSet в EntryList для ожидания конкурирующей блокировки. Когда поток захочет снять блокировку, он вызовет ObjectMonitor. exit() для выхода из блока кода синхронизации. В сочетании с описанием в The Art of Java Concurrent Programming все понятно.

Обновление блокировки до тяжеловесной блокировки также требует двух шагов: (1) аннулирование облегченной блокировки (2) обновление сверхтяжелой блокировки.

Конечно, чтобы отменить облегченную блокировку, содержимое, хранящееся в записи блокировки, сохраненной в фрейме стека, должно быть записано обратно в Mark Work, а затем запись блокировки в фрейме стека должна быть очищена. После этого нужно создать объект ObjectMonitor и сохранить содержимое в Mark Word в ObjectMonitor (чтобы восстановить Mark Word при снятии блокировки, здесь он сохраняется в ObjectMonitor). Так как же найти этот объект ObjectMonitor? Ха-ха, да, просто запишите указатель на объект ObjectMonitor в Mark Word. Как изменить и заменить содержимое в Mark Word? Конечно это будет CAS!

Содержимое Mark Word, заблокированного в виде тяжеловесного мьютекса, выглядит следующим образом:

重量级锁.jpg

Видно, что 30 бит используются в Mark Word для сохранения указателя на ObjectMonitor, а бит флага блокировки равен 10, что указывает на тяжеловесную блокировку.

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

Изменения в форме замка

Теперь мы можем ответить на вопрос «Как выглядят замки в Java?» в начале статьи: в разных состояниях замки имеют разную форму.

Когда блокировка существует как предвзятая блокировка, блокировка является идентификатором потока в Mark Word. В настоящее время сама нить является ключом для открытия блокировки. «ID-карта» какого потока хранится в Mark Word, какой поток получает замок.

Когда блокировка существует как упрощенная блокировка, блокировка представляет собой запись блокировки, на которую указывает запись блокировки в фрейме стека в Mark Word. Ключом в это время является сайт, который является стеком виртуальной машины. в стек попадет запись блокировки.

Когда блокировка существует как тяжеловесная блокировка, блокировкой является ObjectMonitor, реализация Monitor на C++, а ключом в это время является владелец в ObjectMonitor. Тот, на кого указывает владелец, получает замок.

В предыдущем вопросе мы сказали, что 32-битная виртуальная машина Mark Word имеет всего четыре байта, возможно ли, что блокировка существует полностью в этих четырех байтах? Это предложение совершенно неверно до Jdk1.6 и верно в некоторых случаях после Jdk1.6. Теперь вы лучше понимаете это предложение?

Реальный мир разблокирован в мире, - наши люди. Через предыдущее понимание, который является замком в мире программы? Да, это нить.

Оглядываясь на вопросы в начале статьи, легко дать ответ: оказывается, все действительно началось с объекта блокировки, используемого Synchronized!

О КАС

Хотя Synchronized, подвергшийся ряду оптимизаций, имеет гораздо лучшую производительность, чем оригинал, бизнес все больше стремится к малой задержке и высокой скорости отклика, а метод управления параллелизмом CAS, представленный оптимистичным управлением параллелизмом, становится все более и более популярным. Можно видеть, что CAS имеет хороший прикладной эффект при неблокирующей атомарной замене.Интересно, что, согласно предыдущему пониманию, большое количество CAS используется в процессе обновления Synchronized для выполнения неблокирующей модификации и замены Mark Word. , Стоит учиться во многих отношениях.


Спасибо за ваше терпение увидеть здесь, я надеюсь, что эта статья может принести вам некоторую помощь в изучении замков!

Если вы считаете, что это хорошо, пожалуйста, поставьте лайк Ваша поддержка и поощрение являются движущей силой моего творчества!