Реализация Deadly Synchronized Bottom — Введение

Java задняя часть JVM

оsynchronizedБазовая реализация , есть много статей в Интернете. Но во многих статьях либо автор вообще не читал код, а просто обобщал и копировал его из других статей в интернете, что неизбежно допускало какие-то ошибки;

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

Реализация Deadly Synchronized Bottom — Введение

Реализация нижнего уровня Deadly Synchronized — предвзятая блокировка

Базовая реализация Deadly Synchronized — облегченная блокировка

Реализация нижнего уровня Deadly Synchronized — усиленная блокировка

Больше статей смотрите в личном блоге:GitHub.com/farmer Джон Брат…

Реализации потребовалось около двух недель, чтобы посмотреть код (столько времени потребовалось, чтобы немного стыдиться, в основном потому, что я не знаком с C++, базовым механизмом JVM, кодом отладки JVM и сборки).synchronizedЯ в основном прочитал соответствующий код, включая добавление журналов в JVM, чтобы проверить свою гипотезу.synchronizedЭта часть имеет относительно полное и ясное понимание, но уровень ограничен, и некоторые детали неизбежно являются некоторыми упущениями, и я надеюсь, что вы можете меня исправить.

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

Версия JVM, которую я вижу, это jdk8u, конкретный номер версии и код можно найти вздесьВидеть.

Введение в синхронизированный

Есть две основные семантики для реализации синхронизации в Java:synchronizedМетоды иsynchronizedблок, давайте посмотрим демо:

public class SyncTest {
    public void syncBlock(){
        synchronized (this){
            System.out.println("hello block");
        }
    }
    public synchronized void syncMethod(){
        System.out.println("hello method");
    }
}

Когда SyncTest.java компилируется в файл класса,synchronizedключевые слова иsynchronizedБайт-код метода немного отличается, мы можем использоватьjavap -vКоманда для просмотра информации о байт-коде JVM, соответствующей файлу класса, часть информации выглядит следующим образом:

{
  public void syncBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter				 	  // monitorenter指令进入同步块
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String hello block
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit						  // monitorexit指令退出同步块
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit						  // monitorexit指令退出同步块
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
 

  public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED      //添加了ACC_SYNCHRONIZED标记
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String hello method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
 
}

Как видно из китайских комментариев выше, дляsynchronizedключевые слова,javacВо время компиляции соответствующийmonitorenterиmonitorexitв соответствии с инструкциямиsynchronizedВход и выход синхронизированных блоков, есть дваmonitorexitПричина для инструкции: чтобы гарантировать, что блокировка может быть снята, даже если будет выдано исключение, поэтомуjavacДобавлен неявный try-finally для блоков синхронизированного кода, в котором будет вызываться finallymonitorexitкоманда на снятие блокировки. И дляsynchronizedметод,javacсгенерировалACC_SYNCHRONIZEDключевое слово, когда JVM делает вызов метода, обнаруживается, что вызываемый методACC_SYNCHRONIZEDМодифицированный, он сначала попытается получить блокировку.

В нижней части JVM для обоихsynchronizedРеализация семантики примерно такая же, и одна из них будет выбрана для подробного анализа в следующих разделах.

Поскольку данная статья направлена ​​на анализsynchronizedПринцип реализации , поэтому не буду вдаваться в подробности некоторых проблем его использования.Друзья, которые не понимают, могут глянуть.эта статья.

Несколько форм замков

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

До JDK 1.6,synchronizedПрисутствуют только традиционные запорные механизмы, что оставляет разработчикуsynchronizedСоздается впечатление плохой работы ключевых слов по сравнению с другими механизмами синхронизации.

В JDK 1.6 были введены два новых механизма блокировки: предвзятые блокировки и облегченные блокировки, которые были введены для решения проблем с производительностью, вызванных использованием традиционных механизмов блокировки в сценариях, где нет многопоточной конкуренции или вообще нет конкуренции.

Прежде чем рассматривать реализацию этих различных механизмов блокировки, давайте сначала разберемся с заголовком объекта, который является основой для реализации различных механизмов блокировки.

заголовок объекта

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

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

В JVM объект будет иметь заголовок объекта в дополнение к собственным данным в памяти.Для обычных объектов в заголовке объекта есть два типа информации:mark wordи указатели типов. Кроме того, для массивов будет запись данных о длине массива.

Указатель типа — это указатель на объект класса, к которому принадлежит объект,mark wordHashCode, возраст генерации сборщика мусора, статус блокировки и другая информация для хранения объектов. на 32-битных системахmark wordДлина составляет 32 байта или 64 байта в 64-разрядных системах. Для того, чтобы хранить больше данных в ограниченном пространстве, формат его хранения не является фиксированным Формат каждого состояния в 32-битной системе следующий:

image

Вы можете видеть, что информация о блокировке также существует в объекте.mark wordсередина. Когда состояние объекта смещено,mark wordСохраняется смещенный идентификатор потока; когда состояние легко заблокировано,mark wordСохраняет указатель на стек потокаLock Recordуказатель; когда состояние является тяжеловесной блокировкой (раздутой), указатель на объект монитора в куче.

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

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

В состоянии тяжеловесной блокировки объектmark wordуказатель на объект монитора в куче.

Объект монитора включает в себя несколько ключевых полей: cxq (ContentionList на рисунке ниже), EntryList, WaitSet, owner.

Среди них cxq, EntryList и WaitSet — все структуры связанных списков ObjectWaiter, и владелец указывает на поток, удерживающий блокировку.

1517900250327

Когда поток пытается получить блокировку, если блокировка уже занята, он инкапсулирует поток как объект ObjectWaiter и вставляет его в конец очереди cxq, а затем приостанавливает текущий поток. Когда поток, удерживающий блокировку, освобождает блокировку, он перемещает все элементы в cxq в EntryList и пробуждает головной поток EntryList.

Если поток вызывает в синхронизированном блокеObject#waitметод, ObjectWaiter, соответствующий потоку, будет удален из EntryList и добавлен в WaitSet, после чего блокировка будет снята. Когда ожидающий поток будет уведомлен, соответствующий ObjectWaiter будет перемещен из WaitSet в EntryList.

Вышеприведенное является лишь кратким описанием тяжеловесного процесса блокировки, который включает в себя множество деталей, например, откуда берется объект ObjectMonitor? При снятии блокировки перемещать элементы в cxq в хвост или голову EntryList? При уведомлении переместить ObjectWaiter в конец или начало EntryList?

Для конкретных деталей это будет проанализировано в статье о тяжеловесных замках.

Легкий замок

Разработчики JVM обнаружили, что во многих случаях при выполнении Java-программы код в синхронизированном блоке не конкурирует, и разные потоки поочередно выполняют код в синхронизированном блоке. В этом случае тяжелые замки не нужны. Поэтому JVM вводит концепцию облегченных блокировок.

Прежде чем поток выполнит синхронизированный блок, JVM сначала создастLock Record, который включает заголовок для храненияmark word(официально называетсяDisplaced Mark Word) и указатель на объект. Правая часть изображения ниже представляет собойLock Record.

img

процесс блокировки

1. Создайте стек потоковLock Record, положи этоobj(т.е. ссылка на объект на приведенном выше рисунке) Поле указывает на объект блокировки.

2. Напрямую передать инструкцию CASLock RecordАдрес хранится в заголовке объектаmark word, если объект находится в свободном от блокировки состоянии, модификация прошла успешно, что означает, что поток получил облегченную блокировку. Если это не удается, перейдите к шагу 3.

3. Если текущий поток уже удерживает блокировку, это означает повторный вход в блокировку. настраиватьLock Recordпервая часть(Displaced Mark Word) имеет значение null, которое действует как счетчик повторного входа. Тогда это заканчивается.

4. Переход к этому моменту указывает на то, что соревнование произошло и его необходимо расширить до тяжеловесного замка.

процесс разблокировки

1. Пройдите по стеку потоков и найдите всеobjполе, равное текущему объекту блокировкиLock Record.

2. ЕслиLock RecordизDisplaced Mark Wordимеет значение null, что означает, что это повторный вход, будетobjПродолжить после установки нуля.

3. ЕслиLock RecordизDisplaced Mark WordЕсли он не нулевой, используйте инструкцию CAS для преобразованияmark wordвернуться кDisplaced Mark Word. Продолжайте в случае успеха, в противном случае расширяйтесь до тяжелой блокировки.

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

Java — это язык, поддерживающий многопоточность, поэтому во многих сторонних пакетах и ​​базовых библиотеках для обеспечения нормальной работы кода в случае многопоточности, которую мы часто называем потокобезопасностью, напримерsynchronizedТакая семантика синхронизации. Но когда приложение действительно работает, вполне вероятно, что только один поток вызовет соответствующий метод синхронизации. Например, следующая демонстрация:

import java.util.ArrayList;
import java.util.List;

public class SyncDemo1 {

    public static void main(String[] args) {
        SyncDemo1 syncDemo1 = new SyncDemo1();
        for (int i = 0; i < 100; i++) {
            syncDemo1.addString("test:" + i);
        }
    }

    private List<String> list = new ArrayList<>();

    public synchronized void addString(String s) {
        list.add(s);
    }

}

В этой демонстрации для обеспечения безопасности потоков при манипулировании списком добавлен метод addString.synchronizedОднако на практике этот метод вызывается только одним потоком.Для облегченных блокировок каждый раз, когда вызывается addString, выполняется операция CAS для блокировки и разблокировки; для тяжелых блокировок также блокируется одна или несколько операций CAS (квантификаторы «один» и «несколько» здесь только для демонстрации, применимы не ко всем сценариям).

В JDK1.6, чтобыПовышение производительности, когда объект используется в качестве объекта блокировки только одним потоком в течение длительного периода времени., вводится смещенная блокировка. Когда блокировка получена в первый раз, будет операция CAS. После этого поток получает блокировку и выполняет только несколько простых команд вместо относительно дорогих команд CAS. Давайте посмотрим, как выполняется смещенная блокировка.

создание объекта

Когда в JVM включен режим предвзятой блокировки (он включен по умолчанию в 1.6 и выше), при создании нового объекта, если класс, к которому принадлежит объект, не выключает режим предвзятой блокировки (когда будет включен режим предвзятой блокировки класса быть отключенным? Далее будет сказано, что все классы по умолчанию. Режим смещения включен), только что созданный объектmark wordбудет отклоняемое состояние, когдаmark word中идентификатор потока (см. выше под предвзятым состояниемmark wordformat) равен 0, что указывает на то, что ни один поток не является предвзятым, также известным как анонимно предвзятый.

процесс блокировки

случай 1: когда объект блокируется потоком в первый раз и обнаруживается, что он находится в анонимном предвзятом состоянии, инструкция CAS будет использоваться дляmark wordИдентификатор потока изменяется с 0 на текущий идентификатор потока. Если это удается, это означает, что смещенная блокировка получена, и код в синхронизированном блоке продолжает выполняться. В противном случае смещенная блокировка будет аннулирована и преобразована в облегченную блокировку.

случай 2: когда смещенный поток снова входит в блок синхронизации, обнаруживается, что объект блокировки смещен к текущему потоку.После прохождения некоторых дополнительных проверок (подробности см. В следующей статье) в стек будет добавлен новый поток текущего потока.Displaced Mark WordпустойLock Record, а затем продолжить выполнение кода синхронизированного блока, поскольку манипулируется стеком, принадлежащим потоку, поэтому нет необходимости использовать инструкцию CAS; видно, что в режиме смещенной блокировки, когда смещенный поток пытается снова получить блокировку, только несколько. В этом случае будет достаточно простой операции,synchronizedНакладные расходы на производительность, вызванные ключевыми словами, в основном незначительны.

случай 3. Когда другие потоки войдут в синхронизированный блок и обнаружат, что уже существует смещенный поток, они войдут в блокОтменить блокировку смещенияЛогика , вообще говоря, будет вsafepointПроверьте, жив ли смещенный поток. Если он жив и все еще находится в блоке синхронизации, блокировка будет обновлена ​​до облегченной блокировки. Исходный смещенный поток по-прежнему будет владеть блокировкой, а текущий поток войдет в процесс расширения блокировки. логика; если смещение Поток заголовка объекта больше не существует или не находится в синхронизированном блоке,mark wordИзмените на разблокированный, а затем обновите до облегченного замка.

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

процесс разблокировки

Когда другие потоки пытаются получить блокировку, поток смещается в соответствии с обходомlock recordчтобы определить, выполняет ли поток код в синхронизированном блоке. Таким образом, разблокировка смещенной блокировки очень проста, и доступна только последняя в стеке.lock recordизobjПоле имеет значение null. Следует отметить, что на этапе разблокировки замка смещенияОн не изменяет идентификатор потока в заголовке объекта.

На следующем рисунке показан процесс перехода из состояния блокировки:

img

Кроме того, блокировка смещения по умолчанию запускается не сразу.После запуска программы обычно происходит задержка в несколько секунд.Можно использовать команду-XX:BiasedLockingStartupDelay=0отключить задержку.

Массовый повтор и отмена

Как видно из вышеприведенного процесса блокировки-разблокировки смещенных блокировок, когда только один поток неоднократно входит в блок синхронизации, накладные расходы на производительность, вызванные смещенными блокировками, можно в основном игнорировать, но когда другие потоки пытаются получить блокировку, им необходимо Подожди покаsafe pointКогда смещенная блокировка отменяется до состояния блокировки или обновляется до упрощенной/тяжелой блокировки.safe pointМы часто упоминаем это слово в GC, которое обозначает состояние, в котором все потоки приостановлены (вероятно, это означает), вы можете увидеть это для деталейстатья. Короче говоря, отмена смещенных блокировок имеет определенную цену: если в самой среде выполнения существует многопоточная конкуренция, существование смещенных блокировок не только не может улучшить производительность, но и приводит к снижению производительности. Поэтому в JVM был добавлен механизм пакетного изменения/отмены.

Возможны две ситуации: (см. официальнуюбумагаРаздел 4):

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

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

Механизм массового повторного смещения предназначен для решения первого сценария. Массовый отзыв предназначен для решения второго сценария.

Метод заключается в поддержании счетчика отзыва смещенной блокировки для каждого класса в единице класса. Каждый раз, когда объект этого класса подвергается операции смещенного отзыва, счетчик равен +1. Когда это значение достигает порога повторного смещения (по умолчанию 20), JVM считает, что существует проблема с блокировкой смещения класса, поэтому она выполнит пакетное повторное смещение. Каждый объект класса будет иметь соответствующийepochполя, каждый из объектов в смещенном состоянии блокировкиmark word中Так же есть это поле, его начальное значение при создании объекта, в классеepochзначение . Каждый раз, когда происходит пакетное повторное смещение, значение равно +1, и стеки всех потоков в JVM просматриваются, чтобы найти все блокировки смещения в заблокированном состоянии класса и добавить их.epochполе на новое значение. В следующий раз, когда будет получена блокировка, текущий объектepochстоимость и классepochЕсли он не равен, даже если в данный момент он смещен к другим потокам, операция отмены не будет выполнена, а будет удалена напрямую операцией CAS.mark wordИзмените идентификатор потока на идентификатор текущего потока.

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

End

на ЯвеsynchronizedСуществует три типа блокировок: смещенные блокировки, облегченные блокировки и тяжелые блокировки, которые соответствуют трем ситуациям, в которых блокировка удерживается только одним потоком, поочередно удерживается разными потоками и многопоточностью, конкурирующей за блокировки. Если условия не выполняются, замки будут модернизированы в следующем порядке: смещенные замки -> легкие замки -> тяжелые замки. Блокировки JVM также можно понизить, но условия очень жесткие и не входят в предмет нашего обсуждения. Эта статья в основном о JavasynchronizedСделайте базовое введение, а более подробный анализ будет позже.