На этот раз досконально изучите ключевое слово synchronized в Java.

Java
На этот раз досконально изучите ключевое слово synchronized в Java.

Многопоточный параллелизм — очень важная часть языка Java, и в то же время это также сложность в его основе. Это важно, потому что многопоточность является часто используемым знанием в повседневной разработке, и это сложно, потому что существует так много знаний, связанных с многопоточным параллелизмом, и нелегко полностью понять знания, связанные с параллелизмом Java. По этой причине параллелизм в Java стал одним из самых частых вопросов на собеседованиях по Java. В этой серии статей систематизировано понимание параллелизма в Java с точки зрения модели памяти Java, ключевого слова volatile, ключевого слова synchronized, ReetrantLock, класса параллелизма Atomic и пула потоков. Изучив эту серию статей, вы получите глубокое понимание роли ключевого слова volatile, принципа реализации синхронизированных блокировок, блокировок очередей AQS и CLH, четкого понимания спин-блокировок, предвзятых блокировок, оптимистичных блокировок, пессимистические блокировки и т. д. Ослепительное знание параллелизма.

Серия статей о многопоточном параллелизме:

На этот раз тщательно изучите модель памяти Java и ключевое слово volatile.

На этот раз досконально изучите ключевое слово synchronized в Java.

На этот раз я досконально разобрался с принципом реализации ReentranLock в Java.

На этот раз полностью изучите атомарный класс Atomic в параллельных пакетах Java.

Глубокое понимание механизма ожидания и пробуждения потоков Java (1)

Глубокое понимание механизма ожидания и пробуждения потоков Java (2)

Конец серии статей о параллелизме в Java: досконально изучите принцип работы пула потоков Java.

Серия Java Concurrency: принцип ThreadLocal на самом деле очень прост

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

Давайте порекомендуем его, прежде чем начать.AndroidNoteЭто репозиторий GitHub, здесь мои учебные заметки, а также источник моего первого черновика статьи. В этом репозитории собраны обширные знания о Java и Android. Это относительно систематизированная и всеобъемлющая база знаний Android. Это также редкая книга для интервью для студентов, которые готовятся к собеседованию Добро пожаловать на домашнюю страницу репозитория GitHub.

1. Базовое использование синхронизированного

В прошлой статье подробно объяснялось ключевое слово volatile.Мы знаем, что ключевое слово volatile может гарантировать видимость и упорядоченность общих переменных, но не гарантирует атомарность. Если вы хотите обеспечить видимость и порядок общих переменных, но также хотите обеспечить атомарность, тогда ключевым словом synchronized будет хороший выбор.

Использование synchronized очень просто: его можно использовать для изменения методов экземпляра и статических методов, а также для изменения блоков кода. Обратите внимание, что synchronized — это блокировка объекта, то есть блокировка объекта. Следовательно, независимо от того, какой метод используется, синхронизированный должен иметь объект блокировки.

1. Модифицированные методы экземпляра

Чтобы изменить метод экземпляра с помощью synchronized, вам нужно всего лишь добавить в метод ключевое слово synchronized.

public synchronized void add(){
       i++;
}

На этом этапе синхронизированный заблокированный объект является экземпляром самого метода.

2. Украсьте статические методы

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

public static synchronized void add(){
       i++;
}

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

3. Украсьте блоки кода

Синхронизированный измененный блок кода должен быть передан в объект.

public void add() {f
    synchronized (this) {
        i++;
    }
}

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

Дело не в этом.У вас есть вопрос, как ключевое слово synchronized блокирует объект для достижения синхронизации кода? Если вы хотите понять это, вы должны сначала понять заголовок объекта объекта Java.

Во-вторых, заголовок объекта Java и объект Monitor.

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

  • данные экземпляраХраните данные о атрибутах данных класса, включая информацию о атрибутах родительского класса, эта часть памяти выровнена на 4 байта.
  • Ввод данныхПотому что виртуальная машина требует, чтобы начальный адрес объекта был целым числом, кратным 8 байтам. Данные заполнения не требуются, они нужны только для выравнивания байтов.
  • заголовок объектаВ виртуальной машине HotSpot заголовок объекта разделен на две части: Mark Word (отметить поле) и Class Pointer (указатель типа). Если это массив, то будет и длина массива. Заголовок объекта находится в центре внимания этой главы и подробно обсуждается ниже.

1. Заголовок объекта

Слово метки в заголовке объекта в основном хранит данные времени выполнения самого объекта, такие как хэш-код, возраст генерации GC, статус блокировки, блокировки, удерживаемые потоками, смещенные идентификаторы потоков и смещенные временные метки. При этом Mark Word также записывает информацию об объектах и ​​замках.

Когда объект рассматривается как синхронизированная блокировка синхронизированным ключевым словом, серия операций, связанных с замком, связана с Mark Word. Из-за оптимизации блокировки синхронизации в версии JDK1.6 были введены смещенные замки и легкие замки (подробности о оптимизации блокировки обсуждаются позже). Марка Word хранит различное содержимое в разных состояниях блокировки. Мы принимаем содержание хранения заголовка объекта в 32-разрядном JVM, как показано на рисунке ниже.

object_header.png

Можно четко видеть с фигуры, Mark Word имеет 2-битные данные, используемые для отмены состояния блокировки. Никакая блокировка и смещенная блокирующая флаг I равен 01, легкое состояние блокировки 00, состояние блокировки в тяжелом весе составляет 10.

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

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

Видно, что когда это тяжеловесная блокировка, указатель на объект Monitor сохраняется в MarkWord заголовка объекта. Так что же такое монитор?

2. Объект мониторинга

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

В виртуальной машине HotSpot монитор создаетсяObjectMonitorРеализовано, это класс, реализованный с помощью C++, основная структура данных выглядит следующим образом:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;  // 线程重入次数
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 调用wait方法后的线程会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; // 阻塞队列,线程被唤醒后根据决策判读是放入cxq还是EntryList
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 没有抢到锁的线程会被放到这个队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

В ObjectMonitor есть пять важных частей, а именно _ower, _WaitSet, _cxq, _EntryList и count.

  • _owerОн используется для указания на поток, удерживающий монитор.Его начальное значение равно NULL, что указывает на то, что ни один поток в настоящее время не удерживает монитор. Когда поток успешно удерживает блокировку, идентификатор потока будет сохранен, а _ower будет сброшен в NULL после того, как поток освободит блокировку;
  • _WaitSetПотоки, вызывающие метод ожидания объекта блокировки, будут добавлены в эту очередь;
  • _cxqЭто блокирующая очередь.После пробуждения потока решается, поместить ли его в cxq или EntryList в соответствии с решением;
  • _EntryListНе захваченный блокирующий поток будет поставлен в очередь;
  • countОн используется для записи по количеству раз, поток приобретает блокировку. После успешного приобретения блокировки счет увеличится на 1, и количество будет уменьшаться на 1, когда замок выделяется.

Если поток получает монитор объекта, он установит владельца в мониторе на идентификатор потока, а счетчик в мониторе будет увеличен на 1. Если вызывается метод wait() объекта блокировки, поток освобождает удерживаемый в данный момент монитор, сбрасывает переменную владельца в NULL и уменьшает счетчик на 1. В то же время поток войдет в коллекцию _WaitSet и будет ждать. быть разбуженным.

Кроме того, _WaitSet, _cxq и _EntryList — это все очереди со структурой связанного списка, в которых хранится объект ObjectWaiter, инкапсулирующий поток. Трудно понять роль этих очередей, не углубляясь в виртуальную машину для просмотра соответствующего исходного кода, который будет проанализирован в следующей серии статей. Здесь я кратко описываю отношения между ними следующим образом:

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

Таким образом, видно, что _WaitSet хранит потоки в состоянии WAITING, ожидающие пробуждения. Очередь _EntryList хранит состояние BLOCKED в ожидании блокировки. Очередь _cxq сохраняется только временно и в конечном итоге будет передана в _EntryList для ожидания получения блокировки.

После понимания заголовка объекта и мониторинга, как соединяется с синхронизированным ключевым словом с монитором?

3. Основной принцип реализации синхронизированного

В коде Java мы просто используем ключевое слово synchronized для достижения эффекта синхронизации. Так как же он это сделал? Это требует, чтобы мы дизассемблировали байтовые инструкции с помощью инструмента javap, чтобы выяснить это.

1. Синхронизированные кодовые блоки

Разберите следующий фрагмент кода с помощью javap -v.

public void add() {
    synchronized (this) {
        i++;
    }
}

Можно получить следующие инструкции байт-кода:

public class com.zhangpan.text.TestSync {
  public com.zhangpan.text.TestSync();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void add();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter    // synchronized关键字的入口
       4: getstatic     #2                  // Field i:I
       7: iconst_1
       8: iadd
       9: putstatic     #2                  // Field i:I
      12: aload_1
      13: monitorexit  // synchronized关键字的出口
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit // synchronized关键字的出口
      20: aload_2
      21: athrow
      22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any
}

Из инструкций байт-кода мы видим, что есть две инструкции, monitorenter и monitorexit, в 3-й инструкции и 13-й и 19-й инструкциях метода add соответственно. Кроме того, 4-я, 7-я, 8-я, 9-я и 13-я инструкции на самом деле являются инструкциями i++. Из этого можно сделать вывод, что на вход и выход блока синхронизированного кода в байт-коде будут добавлены инструкции monitorenter и monitorexit. При выполнении инструкции monitorenter поток попытается получить право собственности на монитор, соответствующий объекту, то есть попытается получить блокировку объекта.

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

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

2. Реализация метода синхронизации

Инструкции байт-кода для синхронизированных методов отличаются от инструкций байт-кода для блоков синхронизированного кода. Давайте сначала просмотрим инструкции байт-кода следующего кода через javap -v.

public synchronized void add(){
       i++;
}

После разборки можно получить следующие байтовые инструкции

 public synchronized void add();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 5: 0
        line 6: 10

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

4. Тяжелые замки имеют проблемы с производительностью

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

linux_kernel.png

  • Ядро:По сути, его можно понимать как своего рода программное обеспечение, которое управляет аппаратными ресурсами компьютера и обеспечивает среду, в которой работает прикладная программа верхнего уровня.
  • Пользовательское пространство:Пространство для действий приложений верхнего уровня. Выполнение приложения должно зависеть от ресурсов, предоставляемых ядром, включая ресурсы ЦП, ресурсы хранения, ресурсы ввода-вывода и т. д.
  • Системный вызов:Чтобы приложение верхнего уровня могло получить доступ к этим ресурсам, ядро ​​должно предоставить приложению верхнего уровня интерфейс доступа: системный вызов.

Мы уже упоминали об использовании топовых тяжеловесных средств блокировки монитора. существуетobjectMonitor.cppБудут задействованы Atomic::cmpxchg_ptr, Atomic::inc_ptr и другие основные функции, Выполнение синхронизированного блока, конкурс на блокировку парковки объектов () приостанавливается, конкурс на блокировку потока разблокируется () просыпается. На этот раз будет существовать преобразователь режима ядра операционной системы и режима пользователя, такое переключение будет потреблять большое количество системных ресурсов. Только подумайте, если будет много блокировок программы, то программа будет вызывать частое переключение пользовательского режима и режима ядра, что серьезно повлияет на производительность программы. Это также является причиной низкой эффективности синхронизированных

Чтобы решить эту проблему, в JDK1.6 были введены предвзятые блокировки и облегченные блокировки для оптимизации синхронизации.

Пять, синхронизированная оптимизация блокировки

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

1. Несколько состояний блокировки

Далее давайте подробнее рассмотрим эти состояния блокировки.

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

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

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

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

2).Легкий замок

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

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

3).Спиновая блокировка

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

Спин-блокировки основаны наВ большинстве случаев поток не будет удерживать блокировку слишком долго..如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),不断的尝试获取锁。空循环一般不会执行太多次,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入同步代码。如果还不能获得锁,那就会将线程在操作系统层面挂起,即进入到重量级锁。

Именно так оптимизируются спин-блокировки, и этот метод действительно может повысить эффективность.

2. синхронизированный процесс обновления блокировки

После понимания этих типов блокировок, представленных в jdk1.6, давайте подробно рассмотрим, как шаг за шагом блокируются синхронизированные обновления.

(1) Когда это не рассматривается как блокировка, это обычный объект.Mark Word записывает HashCode объекта, бит флага блокировки равен 01, а бит, смещенный в сторону блокировки, равен 0;

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

(3) Когда поток A снова пытается получить блокировку, JVM обнаруживает, что бит флага объекта блокировки синхронизации равен 01, независимо от того, равна ли блокировка смещения 1, то есть состояние смещения, идентификатор потока, записанный в слове метки. является собственным идентификатором потока A, указывающим, что поток A получил эту предвзятую блокировку и может выполнять код синхронно;

(4) Когда поток B пытается получить блокировку, JVM обнаруживает, что блокировка синхронизации находится в смещенном состоянии, но идентификатор потока в слове метки не записывает B, тогда поток B сначала использует операцию CAS, чтобы попытаться получить блокировку Операция получения блокировки здесь может быть успешной, потому что поток A обычно не освобождает автоматически смещенные блокировки. Если захват блокировки прошел успешно, измените идентификатор потока в Mark Word на идентификатор потока B, что означает, что поток B получил смещенную блокировку и может выполнить код синхронизации. Если блокировка захвата не удалась, перейдите к шагу 5;

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

(6) Если облегченная блокировка не может захватить блокировку, JVM будет использовать спин-блокировку, которая не является состоянием блокировки, а представляет собой только непрерывную попытку захвата блокировки. Начиная с JDK1.7, спин-блокировки включены по умолчанию, а количество спинов определяется JVM. Если захват блокировки прошел успешно, выполните код синхронизации, если нет, перейдите к шагу 7;

(7) Если блокировка по-прежнему не работает после повторной попытки спин-блокировки, блокировка синхронизации будет обновлена ​​до тяжелой блокировки, а флаг блокировки будет изменен на 10. В этом состоянии потоки, не захватившие блокировку, будут заблокированы.

6. Резюме

Можно сказать, что использование ключевого слова synchronized очень простое, но на самом деле не так просто полностью понять, что такое synchronized. Потому что это требует большого количества базовых знаний о виртуальных машинах. В то же время необходимо понимать целевую оптимизацию synchronized в JDK1.6, которая включает в себя много всего. Например, в этой статье не объясняется, что такое CAS.Если вы не понимаете CAS, вам сложно понять процесс эскалации блокировок. Читатели, которые не понимают, должны проверить соответствующую информацию самостоятельно. Эта статья является относительно полной для объяснения синхронизированного. Надеюсь, вы что-то поняли после прочтения.

Справочник и рекомендуемая литература

Глубокое понимание принципа реализации синхронизированного ключевого слова в Java.

Синхронизированный базовый принцип монитора

Диск с одним диском синхронизирован (1) — начиная с печати заголовка объекта Java