Мертвая синхронизированная базовая реализация

интервью Java
Мертвая синхронизированная базовая реализация

Ставьте лайк и смотрите снова, формируйте привычку, ищите в WeChat【Третий принц Ао Бин】В первый раз читать.

эта статьяGitHub github.com/JavaFamilyВключено, и есть полные тестовые площадки, материалы и мой цикл статей для интервью с производителями первой линии.

предисловие

Есть много многопоточных вещей, и они тоже очень интересные, поэтому мое последнее внимание может быть направлено на многопоточность.Интересно, нравится ли вам это?

Прежде чем читать эту статью, прочтите следующие две статьи, чтобы лучше понять:

Volatile

Оптимистичный замок и пессимистичный замок

текст

Сцены

Обычно мы используем Synchronized в следующих сценариях:

  • Измените метод экземпляра, чтобы заблокировать текущий объект экземпляра.

    public class Synchronized {
        public synchronized void husband(){

        }
    }
  • Измените статический метод, чтобы заблокировать объект класса текущего класса.

    public class Synchronized {
        public void husband(){
            synchronized(Synchronized.class){

            }
        }
    }
  • Измените блок кода, укажите заблокированный объект и заблокируйте объект

    public class Synchronized {
        public void husband(){
            synchronized(new test()){

            }
        }
    }

По сути, это методы блокировки, блоки кода блокировки и объекты блокировки.Как они реализуют блокировку?

Перед этим позвольте мне поговорить с вами о составе наших объектов Java.

В JVM объекты делятся в памяти на три области:

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

    • Пометить слово (пометить поле): HashCode, возраст генерации и информация о флаге блокировки объекта сохраняются по умолчанию. Он будет повторно использовать свое собственное пространство для хранения в соответствии с состоянием объекта, то есть данные, хранящиеся в Mark Word, будут изменяться при изменении флага блокировки во время работы.
    • Klass Point (указатель типа): указатель объекта на его метаданные класса, виртуальная машина использует этот указатель, чтобы определить, экземпляром какого класса является объект.
  • данные экземпляра

    • Эта часть в основном хранит информацию о данных класса и информацию о родительском классе.
  • Заполните

    • Поскольку виртуальная машина требует, чтобы начальный адрес объекта был целым числом, кратным 8 байтам, данные заполнения не должны существовать, только для выравнивания байтов.

      Совет: Интересно, вас спрашивали, сколько байтов занимает пустой объект? Это 8 байт, из-за выравнивания и заполнения, если он меньше 8 байт, он автоматически заполнит его для нас.

Мы часто говорим о порядке, видимости, атомарности, и как синхронизированный делает это?

упорядоченность

Я уже говорил в главе «Изменчивость», что ЦП переупорядочивает нашу программу, чтобы оптимизировать наш код.

as-if-serial

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

Например:

int a = 1;
int b = a;

Как не переупорядочить эти два абзаца, значение b зависит от значения a, если a не присвоить первым, то оно будет пустым.

видимость

Также в главе "Изменчивость" я представил структуру памяти современных компьютеров, а также JMM (модель памяти Java). Здесь мне нужно пояснить, что на самом деле JMM не существует, а является набором спецификаций. Эта спецификация описывает многие java-программы. , Правила доступа к переменным (переменные, совместно используемые потоком), а также низкоуровневые сведения о хранении и чтении переменных в памяти и из памяти в JVM, модель памяти Java — это видимость, упорядоченность и атомарность для общих данных. Правила и меры безопасности.

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

атомарность

На самом деле его гарантия атомарности очень проста: достаточно, чтобы только один поток одновременно мог получить блокировку и войти в блок кода.

Это функции, которые мы часто используем при использовании блокировок.

повторный вход

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

Итак, в чем польза повторного входа?

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

бесперебойность

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

Стоит отметить, что метод tryLock объекта Lock может быть прерван.

низкоуровневая реализация

Реализация здесь очень проста, я написал простой класс с методом блокировки и блоком кода блокировки, давайте декомпилируем файл байт-кода и все.

Давайте посмотрим на тестовый класс, который я написал:

/**
 *@Description: Synchronize
 *@Author: 敖丙
 *@date: 2020-05-17
 **/
public class Synchronized {
    public synchronized void husband(){
        synchronized(new Volatile()){

        }
    }
}

После завершения компиляции переходим в соответствующую директорию и выполняем команду javap -c xxx.class для просмотра декомпилированных файлов:

MacBook-Pro-3:juc aobing$ javap -p -v -c Synchronized.class
Classfile /Users/aobing/IdeaProjects/Thanos/laogong/target/classes/juc/Synchronized.class
  Last modified 2020-5-17; size 375 bytes
  MD5 checksum 4f5451a229e80c0a6045b29987383d1a
  Compiled from "Synchronized.java"
public class juc.Synchronized
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#14         // java/lang/Object."<init>":()V
   #2 = Class              #15            // juc/Synchronized
   #3 = Class              #16            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Ljuc/Synchronized;
  #11 = Utf8               husband
  #12 = Utf8               SourceFile
  #13 = Utf8               Synchronized.java
  #14 = NameAndType        #4:#5          // "<init>":()V
  #15 = Utf8               juc/Synchronized
  #16 = Utf8               java/lang/Object
{
  public juc.Synchronized();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ljuc/Synchronized;

  public synchronized void husband();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // 这里
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class juc/Synchronized
         2: dup
         3: astore_1
         4: monitorenter   // 这里
         5: aload_1
         6: monitorexit    // 这里
         7: goto          15
        10: astore_2
        11: aload_1
        12: monitorexit    // 这里
        13: aload_2
        14: athrow
        15: return
      Exception table:
         from    to  target type
             5     7    10   any
            10    13    10   any
      LineNumberTable:
        line 10: 0
        line 12: 5
        line 13: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   Ljuc/Synchronized;
}
SourceFile: "Synchronized.java"

синхронный код

Вы можете увидеть несколько мест, которые я отметил, я упомянул заголовок объекта в начале, он будет связан с объектом монитора.

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

  • Если вы уже являетесь владельцем этого монитора, при повторном входе номер записи будет +1.

  • Таким же образом, когда он выполняетmonitorexit, соответствующий номер записи равен -1, пока он не станет 0, его могут удерживать другие потоки.

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

Синхронный метод

Я не знаю, заметили ли вы специальный флаг в методе.ACC_SYNCHRONIZED.

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

Так что в конечном счете это все равно конкуренция за объект монитора.

monitor

Я столько раз говорил об этом объекте, вы думаете, это ничего, но это не так, исходный код монитора написан на C++, в файле ObjectMonitor.hpp виртуальной машины.

Я посмотрел исходный код, и его структура данных выглядит так:

  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;  // 线程重入次数
    _object       = NULL;  // 存储Monitor对象
    _owner        = NULL;  // 持有当前线程的owner
    _WaitSet      = NULL;  // wait状态的线程列表
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;  // 单向列表
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁状态block状态的线程列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

Этот код на C++ я тоже поместил в свой open source проект, каждый может проверить сам.

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

Все говорили, что знакомый процесс обновления блокировки на самом деле находится в исходном коде. Для получения блокировок вызываются разные реализации. Если они терпят неудачу, вызывается реализация более высокого уровня. Наконец, обновление завершено.

1.5 Тяжелый замок

Когда вы посмотрите на исходный код ObjectMonitor, вы найдете Atomic::cmpxchg_ptr, Atomic::inc_ptr и другие функции ядра, соответствующие потоки — park() и upark().

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

Как насчет пользовательского режима и режима ядра?

Архитектура системы Linux должна быть затронута всеми в университете, и она разделена на пространство пользователя (пространство активности приложения) и ядро.

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

Этот процесс очень сложен и включает в себя передачу многих значений.Я кратко подытожу процесс:

  1. Пользовательский режим помещает некоторые данные в регистры или создает соответствующий стек, указывая на то, что требуются службы, предоставляемые операционной системой.
  2. Пользовательский режим выполняет системный вызов (системный вызов — это наименьшая функциональная единица операционной системы).
  3. ЦП переключается в состояние ядра и переходит в указанное место в соответствующей памяти для выполнения инструкции.
  4. Система вызывает процессор для считывания параметров данных, которые мы занесли в память ранее, и выполнения запроса программы.
  5. После завершения вызова операционная система сбрасывает ЦП в пользовательский режим, возвращает результат и выполняет следующую инструкцию.

Так все и твердят, что до 1.6 это была тяжеловесная блокировка, да, но суть ее веса определяется процессом вызова ObjectMonitor и сложным механизмом работы ядра линукса, потребляет много системных ресурсов, поэтому эффективность низкая.

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

1.6 Оптимизация обновления блокировки

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

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

Простая версия:

Направление обновления:

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

После просмотра его обновления, давайте поговорим о том, как делается каждый шаг.

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

Как я упоминал ранее, заголовок объекта состоит из указателя Mark Word и Klass.Конкуренция за блокировку также является конкуренцией объекта Monitor, на который указывает заголовок объекта.Как только поток удерживает объект, бит флага изменяется на 1, и вводится режим смещения. , и запишет ID этого потока в Mark Word объекта.

Этот процесс использует операцию оптимистической блокировки CAS.Каждый раз, когда входит один и тот же поток, виртуальная машина не выполняет никаких операций синхронизации.Достаточно +1 к биту флага.Если придут разные потоки, CAS выйдет из строя, а это значит, что Блокировка приобретения не удалась.

Предвзятая блокировка включена по умолчанию после версии 1.6 и отключена в версии 1.5. Параметр, который необходимо включить вручную, — это xx:-UseBiasedLocking=false.

Что, если смещенная блокировка закрыта или если несколько потоков конкурируют за смещенную блокировку?

Легкий замок

Все еще связанный с Mark Work, если объект не блокируется, jvm создаст пространство с именем Lock Record в кадре стека текущего потока для хранения копии объекта блокировки Mark Word, а затем сохранит запись блокировки. Владелец in указывает на текущий объект.

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

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

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

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

Spin, тот, который приходит сюда, будет продолжать вращаться, чтобы предотвратить приостановку потока. Как только ресурс может быть получен, он будет пытаться добиться успеха, пока не превысит порог. Размер блокировки по умолчанию составляет 10 раз, и - XX: PreBlockSpin можно изменить.

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

До сих пор я в основном говорил о концепции синхронизированных до и после, все ее хорошо усваивают.

Ссылки: «Программирование с высокой степенью параллелизма», «Лекции программиста Dark Horse», «Углубленное понимание виртуальных машин JVM».

Использовать синхронизацию или блокировку?

Сначала рассмотрим их отличия:

  • Synchronized — это ключевое слово, а нижний слой на уровне JVM все делает за нас, а Lock — это интерфейс и богатый API на уровне JDK.

  • Synchronized автоматически снимает блокировку, в то время как Lock должен снимать блокировку вручную.

  • Синхронизация непрерывна, а блокировка может быть прервана или нет.

  • С помощью Lock вы можете узнать, получил ли поток блокировку, но не с помощью synchronized.

  • Synchronized может блокировать методы и блоки кода, а Lock может блокировать только блоки кода.

  • Lock может использовать блокировки чтения для повышения эффективности многопоточного чтения.

  • Synchronized — это несправедливая блокировка, и ReentrantLock может контролировать, является ли она справедливой блокировкой.

Один из двух находится на уровне JDK, а другой — на уровне JVM.Я думаю, что самая большая разница заключается в том, нужны ли нам богатые API и наши сценарии.

Например, я сейчас ДиДи, у меня утром такси, мой код использует много синхронизации, в чем проблема? Процесс апгрейда замка необратим.После пика мы по-прежнему являемся тяжеловесным замком.Сильно снижается эффективность? Полезно ли вам использовать Lock в настоящее время?

Сценарии надо рассматривать.Я вам скажу какой лучше сейчас это ерунда,потому что кроме дела все технические обсуждения не имеют никакой ценности.

我git上的脑图我每次写完我都会重新更新,大家可以没事去看看。
Каждый раз, когда я заканчиваю писать карту мозга на своем git, я снова обновляю ее, и вы можете пойти и посмотреть ее.

Я Ао Бин, мастер по инструментам, который живет в Интернете.

Чем больше вы знаете, тем больше вы не знаете,талантнаш【Три подряд】Это самая большая движущая сила для создания Bing Bing, увидимся в следующем выпуске!

Примечание. Если в этом блоге есть какие-либо ошибки и предложения, оставьте сообщение!


Статья постоянно обновляется, вы можете искать в WeChat "Третий принц Ао Бин"Прочтите это в первый раз, ответьте [материал] Подготовленные мной материалы интервью и шаблоны резюме крупных заводов первой линии, эта статьяGitHub github.com/JavaFamilyОн был включен, и есть полные тестовые сайты для интервью с крупными заводами.Добро пожаловать в Star.