предисловие
Привет всем, меня зовут Джек Сюй, сегодня я расскажу вам о синхронизации. Эта статья является первой статьей в параллельном программировании.Почему это первая статья?Поскольку параллельное программирование включает в себя слишком много вещей, которые неясны и трудны для понимания, вы можете написать статью, если вытащите какой-либо пункт знаний.Это займет как минимум десять статей, чтобы написать серию параллельного программирования. Я обобщил знания, рассортировал их по категориям и объяснил ясно и понятно в простой для понимания форме. .
Зачем использовать синхронизированный
Эта проблема очень проста, сначала давайте посмотрим на следующий код
Откройте 10 000 потоков, увеличьте переменную count, и результат будет 9998, что, очевидно, небезопасно для потоков. Так почему же это происходит, ответ очень прост
这里稍微解释下为啥会得不到 100(知道的可直接跳过), i++ 这个操作,计算机需要分成三步来执行。
1、读取 i 的值。
2、把 i 加 1.
3、把 最终 i 的结果写入内存之中。
所以,(1)、假如线程 A 读取了 i 的值为 i = 0,(2)、这个时候线程 B 也读取了 i 的值 i = 0。
(3)、接着 A把 i 加 1,然后写入内存,此时 i = 1。(4)、紧接着,B也把 i 加 1,此时线程B中的 i = 1,
然后线程B 把 i 写入内存,此时内存中的 i = 1。也就是说,线程 A, B 都对 i 进行了自增,但最终的结果却是1,不是 2.
В конечном счете, очень многие операции не являются атомарными, поэтому как решить эту проблему, просто добавьте Synchronized
Три характеристики
Приведенный выше пример демонстрирует атомарность. Синхронизация может обеспечить видимость.Согласно событиям — до того, как поток выполнит синхронизированный код, все изменения значений переменных в коде могут быть немедленно видны другим потокам. Последовательность означает, что перестановка инструкций запрещена. Код в блоке кода выполняется последовательно сверху вниз. В конечном счете, три характеристики проблемы параллелизма могут быть гарантированы синхронизированным, то есть синхронизированный является панацеей, и это правильно его использовать. !
инструкции
Синтаксически существует три варианта использования Synchronized:
- Украсьте методы экземпляра
public synchronized void eat(){
.......
.......
}
- Украсьте статические методы
public static synchronized void eat(){
.......
.......
}
- декорированный кодовый блок
public void eat(){
synchronized(this){
.......
.......
}
}
public void eat(){
synchronized(Eat.class){
.......
.......
}
}
Первый и третий эквивалентны, второй и четвертый эквивалентны, это очень просто, ниже приводится сводка по использованию synchronized:
- Выберите объект блокировки, которым может быть любой объект;
- Объект блокировки блокирует блок синхронизированного кода, а не сам себя;
- Несколько потоков разных типов. Если есть код, который должен выполняться синхронно, объект блокировки должен использовать один и тот же объект, удерживаемый всеми потоками;
- Код, который необходимо синхронизировать, заключен в фигурные скобки. Необходимость синхронизации означает, что необходимо гарантировать любой один или несколько элементов атомарности, видимости и упорядоченности. Не ставьте код, не требующий синхронизации, это повлияет на эффективность кода.
блокировка обновления
Что ж, кульминация этой статьи здесь. Слушайте внимательно. В первые дни существования JDK синхронизация называлась тяжеловесной блокировкой, потому что заявка на ресурсы блокировки должна проходить через ядро, системные вызовы и преобразование из пользовательского режима в режим ядра, что неэффективно.JDK1.После версии 6 были проведены некоторые оптимизации.Для снижения накладных расходов на производительность, вызванных получением и освобождением блокировок, были введены концепции смещенных блокировок и облегченных блокировок. Таким образом, вы обнаружите, что в синхронизированном режиме есть четыре состояния блокировки: отсутствие блокировки, смещенная блокировка, облегченная блокировка и усиленная блокировка;
Мы знаем, что синхронизированная блокировка — это объект, объект — это объект, и расположение объекта в куче, как показано на следующем рисунке.
Первые 8 байтов — это ключевое слово, последние 4 байта — это указатель класса, который является классом объекта, People — это People.class, а класс Cat — Cat.class, Данные экземпляра сзади — это конкретный размер полей в вашем классе.Теперь int age составляет 4 байта, имя строки составляет 1 байт на английском языке, 2 байта на китайском языке (количество китайских байтов в String зависит от используемой кодировки, если она имеет тип utf-8 , то китайский занимает от 2 до 3 байт, если это тип GBK, то китайский занимает 2 байта), а последние три элемента не могут делиться на 8, то есть заполняются до кратности 8. На следующем рисунке показано распределение маркерного слова (8 * 8 = 64 бита), а обновление блокировки — это изменение бита флага в уценке.
Все картинки в интернете 32-х битные, а то что я тут нарисовал 64-х битные.Все обнаружили, что всего состояний пять, а использовать два мало, поэтому при использовании 01 одно заимствуется вперед.
Блокировка смещения
Автор виртуальной машины точки доступа обнаружил, что в большинстве случаев код блокировки не только не имеет многопоточной конкуренции, но и всегда получается одним и тем же потоком несколько раз. Итак, исходя из такой вероятности, мы изначально заблокировали смещенную блокировку.Когда поток обращается к блоку кода с блокировкой синхронизации, он сначала попытается сохранить идентификатор текущего потока в заголовке объекта через операцию CAS.
(1) Если метка выполнена успешно, текущий идентификатор потока сохраняется, а затем выполняется блок синхронизированного кода.
(2) Если один и тот же поток заблокирован, нет необходимости в состязании, необходимо только определить, совпадают ли указатели потоков, и синхронизированный блок кода может выполняться напрямую.
(3) Если другие потоки получили смещенную блокировку, эта ситуация указывает на то, что текущая блокировка конкурирует, и поток, который получил смещенную блокировку, должен быть отозван, а удерживаемая им блокировка модернизируется до облегченной блокировки (это операция должна дождаться глобальной безопасной точки, то есть ни один поток не выполняет байт-код) для выполнения
При разработке нашего приложения в большинстве случаев должно быть более двух потоков, поэтому, если включена смещенная блокировка, это увеличит потребление ресурсов для получения блокировки. Таким образом, вы можете включить или выключить смещенную блокировку через параметр jvm UseBiasedLocking.
Легкий замок
Отмените предвзятую блокировку и обновите облегченную блокировку.Каждый поток генерирует LockRecord в своем собственном стеке потоков и использует операцию CAS для установки маркерного слова в качестве указателя на LR своего собственного потока, и успешный поток получает блокировку. В процессе блокировки облегченных блокировок используются спин-блокировки.Применение спин-блокировок на самом деле имеет определенные условия.Если поток выполняет блок кода синхронизации в течение длительного времени, непрерывный цикл этого потока будет потреблять вместо этого ресурсы ЦП.
(1) По умолчанию количество спинов составляет 10 раз, которое может быть изменено -xx: Preblockspin, или количество спиновых нитей превышает половину количества ядер CPU
(2) После JDK1.6 была введена адаптивная блокировка спина, что означает, что количество спинов не фиксировано, а основано на времени предыдущего спина на той же блокировке и владельце блокировки. . Если на том же объекте блокировки спин-ожидание только что успешно захватило блокировку, а поток, удерживающий блокировку, работает, то виртуальная машина будет думать, что этот спин, скорее всего, снова будет успешным, и разрешит спин. длится относительно дольше. Если вращение для определенной блокировки редко удается успешно, процесс вращения может быть пропущен при попытке получить блокировку в будущем, и поток будет заблокирован напрямую, чтобы избежать траты ресурсов процессора.
Обновитесь до усиленного замка после выполнения одного из этих двух условий.
тяжелый замок
В это время Лафайет был предупрежден и обратился за ресурсами к операционной системе, linux mutex, ЦП, вызванному из системы 3-го уровня-0, поток был приостановлен, вошел в очередь ожидания, дождался планирования операционной системы, а затем отображается обратно в пользовательское пространство.
Давайте просто напишем простой фрагмент кода с ключевым словом synchronized. Сначала он компилируется в файл .class, а затем дизассемблируется с помощью javap -c xxx.class. Мы можем получить инструкции по сборке, соответствующие java-коду. Внутри вы можете найти следующие две строки инструкций.Уровень BYTECODE - это ключ к этим двум инструкциям, мониторирующему мониториру, Monitorexit (Примечание. Кодовый блок использует ACC_Synchronized, который представляет собой флаг, основной принцип по-прежнему эти два инструкции)
Java каждый объект связан с монитором блокировки монитора, когда монитор находится в заблокированном состоянии, будет завершено. Нить пытается приобрести владение монитором при выполнении инструкции Monitorenter следующим образом:
- Если номер записи монитора равен 0, поток входит в монитор, а затем номер записи устанавливается равным 1, и поток становится владельцем монитора.
- Если поток уже владеет монитором и просто повторно входит в него, количество записей в мониторе увеличивается на 1.
- Если другие потоки заняли монитор, поток переходит в состояние блокировки до тех пор, пока счетчик входов монитора не станет равным 0, а затем снова пытается завладеть монитором.
Из описанного выше процесса можно увидеть два момента:Первое: монитор реентерабельный, у него счетчик, второе: монитор нечестная блокировка
Монитор реализован с помощью mutexLock (блокировка взаимного исключения) операционной системы.После того, как поток заблокирован, он входит в состояние планирования ядра (Linux), что заставит систему переключаться между режимом пользователя и режимом ядра. режим, который серьезно влияет на работоспособность замка. Блок-схема выглядит следующим образом:
Устранение блокировки
Все мы знаем, что StringBuffer является потокобезопасным, потому что его ключевые методы изменяются с помощью synchronized, но когда мы посмотрим на приведенный выше код, мы обнаружим, что ссылка sb будет использоваться только в методе добавления и не может использоваться другими потоками. .Reference (поскольку это локальная переменная, а стек является закрытым), поэтому sb является невозможным ресурсом для совместного использования, и JVM автоматически устранит блокировку внутри объекта StringBuffer.
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
Суммировать
Что ж, в этой статье очень четко объяснены точки знаний, охватываемые синхронизацией. Синхронизация является наиболее часто используемым способом обеспечения безопасности потоков в параллельном программировании на Java, и ее использование относительно просто. До синхронной оптимизации производительность synchronized была намного хуже, чем у ReentrantLock, но так как в synchronized появились смещенные блокировки и облегченные блокировки (спин-блокировки), производительность у них примерно одинаковая. В случае, когда доступны оба метода, чиновник даже рекомендует использовать синхронизированный, ведь оптимизация синхронизированного основана на технологии CAS в ReentrantLock. Все они пытаются решить проблему блокировки в пользовательском режиме, чтобы избежать блокировки потоков, входящих в режим ядра.