1. Как использовать
Synchronized — это наиболее часто используемый способ обеспечения безопасности потоков в java Роль synchronized в основном имеет три аспекта:
- Убедитесь, что блоки кода доступа к потокам взаимоисключающие, только один метод может войти в критическую секцию за раз.
- Обеспечьте своевременную видимость изменений в общих переменных
- Эффективно решить проблему повторного заказа
Семантически существует три основных варианта использования synchronized:
- Украсьте обычные методы, заблокируйте текущий экземпляр объекта (этот)
- Украсить статические методы, заблокировать текущий объект класса (статические методы принадлежат классам, а не объектам)
- Модифицированный блок кода, объект в скобках заблокирован
2. Принцип реализации
2.1 Блокировка монитора
Лежащая в основе семантика блоков синхронизированного кода основана на блокировке монитора (монитора) внутри объекта, которая выполняется с помощью инструкций monitorenter и monitorexit соответственно. На самом деле ожидание/уведомление также зависит от объекта монитора, поэтому обычно используется в синхронизированных синхронизированных методах или блоках кода. Директива monitorenter вставляется в начало блока синхронизированного кода после компиляции в байт-код, а директива monitorexit вставляется в конец метода и исключения после компиляции в байт-код. JVM должна гарантировать, что каждый монитор должен иметь соответствующий моноорексит.
monitorenter: Каждый объект имеет блокировку монитора (монитор).Когда монитор занят потоком, он будет в заблокированном состоянии.Когда поток выполняет команду monitorenter, он пытается получить право собственности на монитор, то есть пытается получить блокировку объекта. Процесс выглядит следующим образом:
- Если счетчик записей монитора равен 0, поток входит в монитор, а затем устанавливает счетчик записей равным 1, и поток становится владельцем монитора;
- Если поток уже владеет монитором и просто повторно входит в него, счетчик входов монитора +1;
- Если другие потоки заняли монитор, поток блокируется до тех пор, пока счетчик записей монитора не станет равным 0, а затем повторяется попытка получить право собственности на монитор.
monitorexit: Поток, выполняющий monitorexit, должен быть владельцем монитора, соответствующего объектной ссылке. При выполнении инструкции счетчик записей монитора уменьшается на 1. Если счетчик записей равен 0 после уменьшения 1, поток выходит из монитора и больше не является владельцем монитора.Другие потоки, заблокированные монитором, могут попытаться получить право собственности на монитор.
2.2 Состояние потока и переход между состояниями
В HotSpot JVM монитор реализуется ObjectMonitor, и его основная структура данных выглядит следующим образом:
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //持有monitor的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
В ObjectMonitor есть две очереди, _WaitSet и _EntryList, которые используются для сохранения списка объектов ObjectWaiter (каждый поток, ожидающий блокировки, будет инкапсулирован в объект ObjectWaiter), а _owner указывает на поток, содержащий объект ObjectMonitor.
- Когда несколько потоков одновременно обращаются к фрагменту синхронного кода, они сначала входят в _EntryList и ждут, пока блокировка будет заблокирована.
- Когда поток получает монитор объекта, он входит в область владельца, устанавливает переменную _owner в ObjectMonitor для текущего потока и увеличивает счетчик счетчика в мониторе на 1.
- Если поток вызовет метод wait(), текущий удерживаемый монитор будет освобожден, переменная _owner будет восстановлена до нуля, а счетчик будет уменьшен на 1. В то же время поток войдет в коллекцию _WaitSet и будет ждать. проснуться и находится в состоянии ожидания.
- Если текущий поток завершит выполнение, монитор будет освобожден, а значение переменной будет сброшено, чтобы другие потоки могли войти, чтобы получить монитор.
Процесс показан на рисунке ниже:
3. Оптимизация блокировки
После JDK1.6 появились различные методы оптимизации блокировки, такие как облегченные блокировки, смещенные блокировки, адаптивный спин, огрубление блокировки, устранение блокировки и т. д. Все эти методы предназначены для более эффективного решения проблемы конкуренции между потоками, чтобы улучшить производительность. оперативность выполнения программы.
Сократите использование тяжеловесных замков, введя облегченные замки и косые замки. Существует четыре состояния блокировки: состояние без блокировки, блокировка со смещением, облегченная блокировка и усиленная блокировка. Блокировки могут быть улучшены с помощью конкуренции, но блокировки не могут быть понижены после обновления, что означает, что они не могут быть переведены из облегченного состояния блокировки в состояние предвзятой блокировки, а также не могут быть понижены из тяжеловесного состояния блокировки в облегченное состояние блокировки.
无锁状态 → 偏向锁状态 → 轻量级锁 → 重量级锁
3.1 Заголовки объектов
Чтобы понять механизм работы облегченных блокировок и смещенных блокировок, необходимо разобраться в заголовке объекта (Object Header). Заголовок объекта разделен на две части:
1. Mark Word: хранит данные времени выполнения самого объекта, такие как: хэш-код, возраст генерации GC, информацию о блокировке. Эта часть данных является 32-битной и 64-битной в 32-битной и 64-битной JVM соответственно. Принимая во внимание эффективность использования пространства, Mark Word разработан как нефиксированная структура данных, чтобы хранить как можно больше информации в очень маленьком пространстве.32-битное слово Mark показано на следующем рисунке:
2. Сохраняем указатель на данные типа объекта в области методов, если это объект массив, то дополнительно сохраняется длина массива
3.2 Тяжелые замки
Блокировка монитора монитора по существу реализуется с помощью мьютекса Mutex Lock операционной системы, который мы обычно называем重量级锁
. Поскольку ОС должна переключаться из пользовательского режима в основной режим для переключения между потоками, этот процесс преобразования является дорогостоящим и трудоемким, поэтому эффективность синхронизации будет относительно низкой.
Флаг блокировки тяжеловесной блокировки равен '10', а указатель указывает на начальный адрес объекта монитора.Принцип реализации монитора был описан выше.
3.3. Легкие замки
轻量级锁
Это относительно тяжеловесной блокировки, основанной на мьютексе на основе ОС. Его первоначальная цель состоит в том, чтобы уменьшить потребление производительности, вызванное традиционной тяжеловесной блокировкой, использующей мьютекс ОС, без многопоточной конкуренции.
Эмпирическая основа облегченных замков для повышения производительности:对于绝大部分锁,在整个同步周期内都是不存在竞争的
. Если конфликтов нет, облегченные блокировки могут использовать операции CAS, чтобы избежать накладных расходов на мьютексы, тем самым повышая эффективность.
Процесс блокировки легкого замка:
1. Когда поток входит в блок кода синхронизации, JVM сначала создаст пространство с именем Lock Record в кадре стека текущего потока, которое используется для хранения копии текущего слова метки объекта блокировки (официально называемого Displaced). ) Mark Word), указатель владельца указывает на Mark Word объекта. В это время состояние стека и заголовка объекта показано на рисунке:
2. JVM использует операцию CAS, чтобы попытаться обновить Mark Word в заголовке объекта до указателя на Lock Record. Если обновление прошло успешно, перейдите к шагу 3; если обновление не удалось, перейдите к шагу 4.
3. Если обновление прошло успешно, то этот поток владеет блокировкой объекта, а состояние блокировки Mark Word объекта является облегченной блокировкой (бит флага меняется на '00'). На данный момент состояние стека потока и заголовка объекта показано на рисунке:
4. Если обновление не удается, JVM сначала проверяет, указывает ли Mark Word объекта на кадр стека текущего потока.
- Если это так, это означает, что текущий поток уже владеет блокировкой объекта, тогда он может напрямую войти в блок кода синхронизации для продолжения выполнения.
- Если нет, это означает, что объект блокировки был вытеснен другими потоками, и текущий поток попытается прокрутить определенное количество раз, чтобы получить блокировку. Если операция CAS по-прежнему не работает после определенного количества вращений, то
轻量级锁
быть повышен до重量级锁
(Флаг блокировки меняется на '10'), указатель на тяжеловесную блокировку сохраняется в Mark Word, и поток, ожидающий блокировку, переходит в состояние блокировки.
Процесс разблокировки облегченного замка:
1. Замените текущее слово метки объекта данными в смещенном слове метки, скопированном в потоке посредством операции CAS.
2. Если замена прошла успешно, весь процесс синхронизации завершен
3. Если замена не удалась, это означает, что другие потоки пытались получить блокировку, а затем разбудить приостановленный поток, освобождая блокировку.
3.4. Блокировка смещения
轻量级锁
Это использование операции CAS для устранения мьютекса при отсутствии многопоточной конкуренции;偏向锁
При отсутствии многопоточной конкуренции эта синхронизация исключается.
Эмпирическая основа для предвзятых блокировок для повышения производительности:对于绝大部分锁,在整个同步周期内不仅不存在竞争,而且总由同一线程多次获得
. Смещенная блокировка будет смещена в сторону первого потока, который ее получит.Если блокировка не будет получена другим потоком во время следующего выполнения, потоку, удерживающему смещенную блокировку, не нужно снова синхронизироваться. Это удешевляет получение блокировок потоками.
Процесс приобретения смещенного замка:
1. Когда поток выполняет блок синхронизации, когда объект блокировки получен в первый раз, JVM установит состояние блокировки в слове метки объекта блокировки на смещенную блокировку (флаг блокировки равен «01», а флаг смещения равен '1''), и запишите ThreadID потока, который получил блокировку в Mark Word через операцию CAS
2. Если операция CAS прошла успешно. Каждый раз, когда поток, удерживающий смещенную блокировку, входит и выходит из синхронизированного блока, просто проверьте, хранится ли ThreadID текущего потока в Mark Word. Если это так, это означает, что поток получил блокировку без дополнительных операций CAS для блокировки и разблокировки.
3. Если нет, то операция CAS используется для борьбы за блокировку, и конкуренция успешна, тогда замените ThreadID слова метки на ThreadID текущего потока.
Процесс выпуска смещенного замка:
1. Когда поток уже удерживает смещенную блокировку, а другой поток пытается конкурировать за смещенную блокировку, операция CAS по замене ThreadID дает сбой, и смещенная блокировка начинает отзываться. Для отмены предвзятой блокировки необходимо дождаться, пока поток, который первоначально удерживал предвзятую блокировку, достигнет глобальной безопасной точки (в которой никакой байт-код не выполняется), приостановить поток и проверить его статус.
2. Если поток, который первоначально удерживал блокировку смещения, не активен или вышел из блока синхронизированного кода, поток освобождает блокировку. Установите заголовок объекта в состояние без блокировки (флаг блокировки равен «01», флаг смещения равен «0»).
3. Если поток, который изначально удерживал блокировку смещения, не выходит из блока кода синхронизации, он будет обновлен до облегченной блокировки (флаг блокировки равен «00»).
3.5. Резюме
Переходы состояний между смещенными блокировками, облегченными блокировками и тяжелыми блокировками показаны на рисунке (показывая получение и освобождение блокировки, описанные выше):
Ниже приводится сравнение этих типов замков:
3.6 Другие оптимизации
1. Адаптивный спин
自旋锁
: во время синхронизации взаимного исключения потоки приостановки и возобновления должны быть переключены в режим ядра, что оказывает большое влияние на параллелизм производительности. В то же время во многих приложениях состояние блокировки общих данных будет длиться только в течение короткого периода времени, и не стоит приостанавливать и возобновлять потоки на этот короткий период времени. Затем, если несколько потоков выполняются параллельно одновременно, вы можете позволить потоку, запрашивающему блокировку, подождать некоторое время, вращаясь (цикл занятости ЦП выполняет пустые инструкции), чтобы увидеть, быстро ли освободит блокировку поток, удерживающий блокировку. , так что нет необходимости отказываться от времени выполнения процессора.
适应性自旋
: во время процесса получения облегченной блокировки, когда потоку не удается выполнить операцию CAS, ему необходимо выполнить вращение, чтобы получить тяжеловесную блокировку. Если блокировка занята в течение короткого времени, эффект ожидания вращения будет лучше, а если блокировка занята в течение длительного времени, вращающийся поток будет тратить ресурсы ЦП. Самый простой способ решить эту проблему — указать количество спинов, и если блокировка не была получена за ограниченное количество раз (например, 10 раз), приостановить поток традиционным способом и войти в состояние блокировки. После JDK1.6 был введен метод адаптивного вращения.Если на одном и том же объекте блокировки поток вращается в ожидании успешного получения блокировки, а поток, удерживающий блокировку, выполняется, то JVM
Будет сочтено возможным, что спин на этот раз снова успешно получит блокировку, что позволит спину ожидать относительно более длительное время (скажем, 100 раз). С другой стороны, если прокрутка блокировки редко удается успешно, процесс прокрутки будет пропущен, когда блокировка будет получена в будущем, чтобы избежать пустой траты ЦП.
2. Снятие блокировки
Устранение блокировки — это устранение некоторых блокировок, которые, как обнаружено, вряд ли будут иметь общие гонки данных во время работы компилятора. Если предполагается, что в фрагменте кода данные в куче не будут ускользать и не будут доступны другим потокам, вы можете рассматривать их как данные в стеке, думая, что они являются потоко-частными и не нуждаются в блокировке. .
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
return sb.toString();
}
В методе StringBuffer.append() есть синхронизированный блок кода, блокировкой является объект sb, но все ссылки на sb не выходят за пределы метода concatString(), и другие потоки не могут получить к нему доступ. Так что блокировка здесь есть, но после JIT-компиляции она будет благополучно снята, игнорируя синхронизацию и выполняясь напрямую.
3. Огрубление блокировки
Огрубление блокировки означает, что JVM обнаруживает, что серия фрагментарных операций блокирует один и тот же объект, и огрубляет область синхронизации блокировки за пределы всей последовательности операций. Взяв в качестве примера приведенный выше метод concatString(), внутренний StringBuffer.append() будет блокироваться каждый раз, и блокировка будет грубее.Вам нужно только добавить блокировку один раз перед первым append() к последнему append( ).
4. Ссылка
«Глубокое понимание виртуальной машины Java» — Чжоу Чжимин
Синхронизированный механизм Java
Принцип реализации синхронизированного ключевого слова Java
---
Мой блог будет перемещен и синхронизирован с Tencent Cloud + Community, и всем предлагается присоединиться: https://cloud.tencent.com/developer/support-plan?invite_code=12mihsfip6v9b