Оригинальная статья, краткое изложение опыта и жизненные перипетии на всем пути от набора в школу до фабрики А
Нажмите, чтобы узнать подробностиwww.codercc.com
1. Введение в синхронизированный
Прежде чем изучать знания, давайте посмотрим на явление:
public class SynchronizedDemo implements Runnable {
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new SynchronizedDemo());
thread.start();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + count);
}
@Override
public void run() {
for (int i = 0; i < 1000000; i++)
count++;
}
}
开启了10个线程,每个线程都累加了1000000次,如果结果正确的话自然而然总数就应该是10 * 1000000 = 10000000。可就运行多次结果都不是这个数,而且每次运行结果都不一样。 Вот почему? Каковы решения?这就是我们今天要聊的事情。
В предыдущем сообщении блога мы виделиJava-модель памятиНекоторые знания, и было известно, что безопасность потоков в основном исходит из конструкции JMM, которая в основном вызвана основной памятью и рабочей памятью потока.проблемы с видимостью памяти,а такжеПроблемы, вызванные изменением порядка, узнать большепроисходит до правила. Когда поток работает, он имеет свое собственное пространство стека и будет выполняться в своем собственном пространстве стека.Если между несколькими потоками нет общих данных, то есть нет взаимодействия между несколькими потоками для выполнения одной задачи, тогда -threading не может воспользоваться этим преимуществом и не может принести Comes большой ценности. Так как же быть с потокобезопасностью общих данных? Естественная идея состоит в том, что каждый поток читает и записывает эту общую переменную по очереди, так что не будет проблем с безопасностью данных, потому что каждый поток работает с последней версией текущих данных. Затем ключевое слово synchronized в java имеет функцию постановки в очередь каждого потока для работы с общими переменными по очереди. Очевидно, что этот механизм синхронизации очень неэффективен, но синхронизированный — это основа для других реализаций параллельного контейнера, и понимание его значительно улучшит ощущение параллельного программирования.С утилитарной точки зрения это еще и высокочастотная тестовая площадка. для интервью. Ну а теперь поговорим об этом ключевом слове подробнее.
2. Принцип синхронного выполнения
Использование synchronized в коде java может использоваться в блоках кода и методах.В зависимости от местоположения synchronized существуют сценарии использования, как показано в таблице 3.1:
используемое местоположение | Сфера | заблокированный объект | образец кода |
---|---|---|---|
метод | метод экземпляра | объект экземпляра класса | public synchronized void method() { .......} |
статический метод | объект класса | public static synchronized void method1() { .......} | |
кодовый блок | экземпляр объекта | объект экземпляра класса | synchronized (this) { .......} |
объект класса | объект класса | synchronized (SynchronizedScopeDemo.class) { .......} | |
любой экземпляр объекта | экземпляр объекта | final String lock = "";synchronized (lock) { .......} |
Как показано в таблице, синхронизированный может использоваться в методах или в блоках кода.Метод заключается в том, что методы экземпляра и статические методы блокируют объект экземпляра класса и объект класса соответственно. Вместо этого используйте целевой объект в блоке кода в соответствии с замком
Его также можно разделить на три типа, а конкретные данные можно увидеть в таблице. Здесь следует отметить, что если блокировка является объектом класса, даже если несколько объектов-экземпляров являются новыми, они все равно будут заблокированы. Использование synchronized очень просто, так каков же принцип и механизм реализации?
1 Механизм блокировки объекта (монитор)
Теперь, чтобы продолжить анализ конкретной базовой реализации synchronized, есть простой пример кода:
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
System.out.println("hello synchronized!");
}
}
}
Приведенный выше код синхронизируется путем «блокировки» текущего объекта класса через синхронизированный.После компиляции кода Java используйте javap -v SynchronizedDemo .class для просмотра соответствующего байт-кода основного метода следующим образом:
public static void main(java.lang.String[]);
• descriptor: ([Ljava/lang/String;)V
• flags: ACC_PUBLIC, ACC_STATIC
• Code:
• stack=2, locals=3, args_size=1
• 0: ldc #2 // class com/codercc/chapter3/SynchronizedDemo
• 2: dup
• 3: astore_1
• 4: **monitorenter**
• 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
• 8: ldc #4 // String hello synchronized!
• 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
• 13: aload_1
• 14: monitorexit
• 15: **goto** 23
• 18: astore_2
• 19: aload_1
• 20: **monitorexit**
• 21: aload_2
• 22: **athrow**
• 23: **return
Важные байт-коды были отмечены в исходном файле байт-кода, а затем в синхронизированном блоке, вам необходимо получить монитор объекта (также обычно называемый блокировкой объекта) с помощью инструкции monitorenter, прежде чем его можно будет выполнить.После обработки внутренней логики соответствующий метод, освобождает монитор, удерживаемый командой monitorexit, для захвата другими параллельными объектами. Затем код выполняется до оператора goto в строке 15, а затем переходит к инструкции возврата в строке 23, и метод успешно завершается. Кроме того, при ненормальном методе, если монитор не освобождается, нет шансов, что другие параллельные объекты будут заблокированы, и система сформирует состояние тупика, что, очевидно, неразумно.
Следовательно, в случае исключения будет выполнена 20-я строка инструкции для снятия блокировки монитора через monitorexit, а соответствующее исключение будет выброшено через 22-ю строку байт-кода athrow. Из анализа инструкций байт-кода также видно, что использование synchronized имеет удобство неявной блокировки и снятия блокировки, а также блокировка снимается в нештатных ситуациях.
С каждым объектом связан монитор.То, как поток удерживает монитор, и время удержания определяют синхронизированное состояние блокировки и метод обновления синхронизированного состояния. монитор реализован через ObjectMonitor в C++, и код может быть связан через точку доступа openjdk (Korea.open JDK.Java.net/JDK8U/JDK8U…), чтобы загрузить исходный код версии точки доступа в openjdk, конкретный путь к файлу находится в src\share\vm\runtime\objectMonitor.hpp, конкретный исходный код:
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
• _header = NULL;
• _count = 0;
• _waiters = 0,
• _recursions = 0;
• _object = NULL;
• _owner = NULL;
• **_WaitSet** = NULL;
• _WaitSetLock = 0 ;
• _Responsible = NULL ;
• _succ = NULL ;
• _cxq = NULL ;
• FreeNext = NULL ;
• **_EntryList** = NULL ;
• _SpinFreq = 0 ;
• _SpinClock = 0 ;
• OwnerIsThread = 0 ;
• _previous_owner_tid = 0;
}
Основное обслуживание видно из структуры ObjectMonitorWaitSet иВ EntryList есть две очереди для хранения объектов ObjectWaiter.Когда каждый заблокированный поток, ожидающий получения блокировки, будет инкапсулирован в объект ObjectWaiter для входа в очередь, и в то же время, если ресурс блокировки получен, он будет удален из очереди. Кроме того, _owner указывает на поток, который в данный момент содержит объект ObjectMonitor. Схематическая диаграмма ожидания получения блокировки и получения блокировки для удаления из очереди показана на следующем рисунке:
Когда несколько потоков получают блокировки, они сначала_EntryList
Очередь, после того как один из потоков получит монитор объекта, монитор будет_owner
Переменной присваивается значение текущего потока, а счетчик, поддерживаемый монитором, увеличивается на 1. Если текущий поток выполняет логику и завершается, монитор_owner
Переменная очищается, а счетчик уменьшается на 1, позволяя другим потокам конкурировать за монитор. Кроме того, при вызове метода wait() текущий поток войдет в _WaitSet и будет ждать пробуждения, а если он пробуждается и выполнение завершится, то количество состояний также будет сброшено, что также удобно для других потоков. получить монитор.
С точки зрения изменения состояния потока, если вы хотите войти в синхронизированный блок или выполнить синхронизированный метод, вам необходимо сначала получить монитор объекта.Если вы не можете его получить, он перейдет в состояние «БЛОКИРОВАН». показано на следующем рисунке:
Как видно из рисунка выше, доступ любого потока к Объекту должен сначала получить монитор Объекта.Если получение не удается, поток попадет в очередь синхронизации, и состояние потока станет ЗАБЛОКИРОВАННЫМ. Когда держатель монитора освобождается, потоки в очереди синхронизации будут иметь возможность повторно захватить монитор, прежде чем продолжить выполнение.
2 синхронизированных события до отношений
В главе 2 я проанализировал правила «происходит до», одним из которых является правило блокировки монитора: разблокировка того же монитора происходит до блокировки монитора. Чтобы лучше понять параллельную семантику синхронизированного, проанализируйте это правило «происходит до» с помощью примера кода. Пример кода выглядит следующим образом:
public class MonitorDemo {
private int a = 0;
•
public synchronized void writer() { // 1
a++; // 2
} // 3
•
public synchronized void reader() { // 4
int i = a; // 5
} // 6
}
В случае параллелизма, каково значение переменной a, прочитанной на шаге 5? Это необходимо проанализировать с помощью правила «происходит до» Отношение «происходит до» примера кода показано на следующем рисунке:
Два узла, соединенные каждой стрелкой на приведенном выше рисунке, представляют отношение между ними «произошло до», а черный — через.правила порядка программывыведеноПравила блокировки монитораМожно сделать вывод, что поток A освобождает блокировку до того, как блокируется поток B, что обозначено красной линией. Синяя линия проходиттранзитивное правилоДальше производное происходит до отношения. Окончательный вывод заключается в том, что операция 2 происходит раньше, чем 5, что можно вывести из этого соотношения?
Согласно одному из определений «происходит раньше»: если А происходит раньше Б, то результат выполнения А виден Б. Затем в этом примере кода поток A сначала добавляет 1 к общей переменной A. Из отношения 2 происходит до 5 мы видим, что результат выполнения потока A виден потоку B, то есть значение чтения по потоку B равно 1.
3 Семантика памяти при получении и освобождении блокировки
Общие результаты в главе 2 Ядро JMM разделено на две части:Правила «происходит до» и модель абстракции памяти. После анализа отношения «синхронизация происходит раньше» оно все еще не завершено. Далее давайте рассмотрим семантику синхронизированной памяти на основе абстрактной модели памяти Java. Конкретный процесс показан на следующем рисунке:
Для работы потока A из рисунка выше видно, что поток A сначала прочитает значение общей переменной a=0 из основной памяти, а затем скопирует переменную в локальную память потока. Затем переменная a становится равной 1 после обработки данных на основе этого значения, а затем значение записывается в основную память.
Для потока B поток выполнения показан на рисунке выше. Когда поток B получит блокировку, он будет вынужден поделиться значением переменной a из основной памяти, а в это время переменная a уже является самым последним значением. Далее поток B скопирует значение в рабочую память для операции, а также перезапишет его в основную память после завершения той же операции.
С горизонтальной точки зрения поток A и потоки потока воспринимают операции друг друга с данными на основе общих переменных в основной памяти и завершают совместную работу в параллельных сущностях на основе общих переменных Весь процесс выглядит так, как если бы поток A отправляет поток B в поток. B. Предоставляется «уведомление» об изменении данных, и этот механизм связи обусловлен структурой модели параллелизма, основанной на общей памяти.
Благодаря приведенному выше обсуждению у вас должно быть определенное понимание синхронизированного, Его самая большая особенность заключается в том, что только один поток может одновременно получать монитор объекта, чтобы гарантировать, что текущий поток может выполнить соответствующую логику синхронизации, которая выражена в видеВзаимное исключение (исключительность). Естественно, этот метод синхронизации имеет недостаток относительно низкой эффективности.Поскольку процесс синхронизации нельзя изменить, может ли он ускорить получение каждой блокировки или уменьшить вероятность ожидания блокировки? этоПовышение эффективности общей параллельной синхронизации системы за счет локальной оптимизации. Например, в сцене похода к кассе для оплаты предыдущий метод заключался в том, что все выстраивались в очередь, а затем шли к кассе, чтобы расплатиться банкнотами, чтобы получить сдачу. Даже иногда при оплате нужно доставать кошелек и доставать деньги, что занимает много времени. Для процесса оплаты он может быть оптимизирован с помощью онлайн-средств.Теперь оплата может быть завершена путем сканирования QR-кода через Alipay, что также экономит время кассира на сдачу. Хотя всю сцену оплаты по-прежнему нужно ставить в очередь, оптимизация платежа (аналогично получению и снятию блокировки) значительно сократила затраты времени и значительно повысила эффективность работы кассира (общая эффективность параллелизма система). Таким образом, если процесс операции блокировки можно оптимизировать, это также значительно повысит эффективность параллелизма.
Итак, как выполняется оптимизация для синхронизированных? Перед дальнейшим анализом вам необходимо понять эти две концепции: 1. Работа CAS 2. Заголовок объекта Java.
3.1 Работа CAS
3.1.1 Что такое CAS?
При использовании блокировок поток, получающий блокировку, являетсяСтратегия пессимистической блокировки, то есть предполагается, что каждый раз при выполнении кода критической секции будет возникать конфликт, поэтому, когда текущий поток получает блокировку, он также блокирует получение блокировки другими потоками. Операции CAS (также известные как операции без блокировки)оптимистичная стратегия блокировкиПредполагается, что не будут конфликтов, когда все темы доступа к общим ресурсам, и поскольку не будут конфликтов, они, естественно, не блокируют операции других потоков. Следовательно, нить не будет заблокирована и заблокирована. Так что, если есть конфликт? Операция без блокировки использует CAS (сравнение и своп), также известный как сравнение и своп, чтобы определить, существует ли конфликт между потоками. Если есть конфликт, текущая операция повторяется до тех пор, пока нет конфликта.
3.1.2 Процесс работы CAS
Процесс сравнения и обмена CAS можно обычно понимать как CAS(V,O,N), который содержит три значения:V фактическое значение, хранящееся по адресу памяти; O ожидаемое значение (старое значение); N обновленное новое значение.. Когда V и O одинаковы, то есть старое значение и фактическое значение в памяти совпадают, что указывает на то, что значение не было изменено другими потоками, то есть старое значение O является последним значением в момент времени. присутствует, и естественно может быть присвоено новое значение N. Дайте V. Напротив, V и O не совпадают, указывая на то, что значение было изменено другими потоками, а старое значение O не является значением последней версии, поэтому новое значение N не может быть присвоено V, а V можно вернуть. Когда несколько потоков используют CAS для манипулирования переменной, только один поток успешно обновится, а остальные потерпят неудачу. Неудачные потоки попытаются снова, и, конечно же, вы можете приостановить потоки.
Реализация CAS требует поддержки набора аппаратных инструкций.После JDK1.5 виртуальная машина может использовать предоставленный процессором.CMPXCHGРеализация директивы.
Synchronized VS CAS
Основная проблема ветерана Synchronized (до оптимизации) заключается в следующем: при наличии конкуренции потоков будут проблемы с производительностью, вызванные блокировкой потоков и блокировками пробуждения, потому что это синхронизация с взаимоисключением (блокирующая синхронизация). CAS не является произвольной приостановкой между потоками.При сбое операции CAS будет предпринята определенная попытка вместо длительной операции приостановки и пробуждения, поэтому ее также называют неблокирующей синхронизацией. Это основное различие между ними.
3.1.3 Сценарии применения CAS
В пакете JUC есть много классов реализации, использующих CAS.Можно сказать, что он поддерживает реализацию всего пакета concurrency.В реализации Lock будет CAS для изменения переменной состояния.Классами реализации в пакете atomic являются почти все реализовано с помощью CAS, об этом Конкретная сцена реализации будет подробно рассмотрена позже, а сейчас у меня сложилось впечатление (улыбается).
3.1.4 Проблемы с CAS
1. Проблема АВАЭто интересная проблема, потому что CAS проверяет, изменилось ли старое значение. Например, старое значение A становится B, а затем снова становится A. При выполнении CAS проверяется и выясняется, что старое значение не изменилось и по-прежнему является A, но на самом деле оно изменилось. Решение может следовать методу оптимистической блокировки, обычно используемому в базе данных, и добавление номера версии может решить эту проблему. Исходный путь изменения A->B->A становится 1A->2B->3C. Java такой отличный язык.Конечно, AtomicStampedReference предоставляется в пакете atomic после java 1.5 для решения проблемы ABA.Решение такое.
2. Спин слишком долго
При использовании CAS неблокирующая синхронизация означает, что поток не будет приостановлен, а будет вращаться (не что иное, как бесконечный цикл) для следующей попытки.Если время вращения слишком велико, это будет потреблять много производительности. Если JVM может поддерживать инструкцию паузы, предоставляемую процессором, будет определенное повышение эффективности.
3. Можно гарантировать только атомарные операции над общей переменной
CAS гарантирует атомарность при выполнении операций с общей переменной, но не может гарантировать атомарность при выполнении операций с несколькими общими переменными. Одним из решений является использование объектов для интеграции нескольких общих переменных, то есть переменными-членами в классе являются эти общие переменные. Затем выполните операцию CAS для этого объекта, чтобы обеспечить его атомарность. AtomicReference предоставляется в atomic для обеспечения атомарности между ссылочными объектами.
3.2 Заголовок объекта Java
При синхронизации получается монитор объекта, то есть блокировка объекта получается. Итак, как понять блокировку объекта? Это не что иное, как признак, похожий на объект, тогда этот признак является заголовком объекта, хранящимся в объекте Java. Хэш-код, возраст поколения и бит флага блокировки объекта, хранящиеся по умолчанию в Mark Word в заголовке объекта Java. 32 является структурой хранения JVM Mark Word по умолчанию (Примечание: заголовок java-объекта и следующие изменения состояния блокировки взяты из книги «Искусство параллельного программирования на Java», которая, как мне кажется, написана достаточно хорошо, поэтому я не должны организовать свои собственные языковые курсы.):
Как показано в Mark Word, такая информация, как hasdcode, значение возраста и флаг блокировки, будет сохранена по умолчанию.
В Java SE 1.6 есть 4 состояния блокировки, уровни от низкого до высокого:Разблокированное состояние, смещенное состояние блокировки, упрощенное состояние блокировки и тяжелое состояние блокировки, эти состояния будут постепенно обостряться с конкуренцией.Замки можно улучшать, но нельзя понижать, что означает, что смещенная блокировка не может быть понижена до смещенной блокировки после обновления до облегченной блокировки. Целью этой стратегии укрупнения блокировки, а не понижения уровня, является повышение эффективности получения и снятия блокировок. MarkWord объекта изменится на следующее изображение:
3.2 Блокировка смещения
Авторы HotSpot в ходе исследований обнаружили, что в большинстве случаев блокировки не только не имеют многопоточной конкуренции, но и всегда запрашиваются одним и тем же потоком несколько раз, а предвзятые блокировки вводятся для того, чтобы сделать стоимость захвата потока более высокой. замки ниже.
Получение блокировки смещения
Когда поток обращается к синхронизированному блоку и получает блокировку, онзаголовок объектаиЗаблокировать запись в кадре стекаИдентификатор потока смещения блокировки хранится в замке.В дальнейшем потоку не нужно выполнять CAS-операции для блокировки и разблокировки при входе и выходе из блока синхронизации.Просто проверьте, хранит ли Mark Word в заголовке объекта блокировка смещения, указывающая на текущий поток. Если проверка прошла успешно, поток получил блокировку. Если проверка не пройдена, необходимо снова проверить, установлен ли флаг предвзятой блокировки в Mark Word в 1 (указывая, что в данный момент это предвзятая блокировка): если он не установлен, используйте CAS для борьбы за блокировку; если он установлен, попробуйте использовать CAS для преобразования заголовка объекта Смещенные точки блокировки на текущий поток
Отзыв предвзятых блокировок
Блокировка смещения используетПодождите, пока не возникнет конфликт, прежде чем снимать блокировку.Механизм, поэтому, когда другие потоки пытаются конкурировать за смещенную блокировку, поток, удерживающий смещенную блокировку, освобождает блокировку.
Как показано на рисунке, для отмены предвзятой блокировки требуется ожидание.глобальная точка безопасности(В данный момент байт-код не выполняется). Сначала он приостановит поток, удерживающий смещенную блокировку, затем проверит, жив ли поток, удерживающий смещенную блокировку, если поток не активен, установите заголовок объекта в состояние без блокировки; если поток все еще жив, стек со смещенной блокировкой будут удалены Выполнить, просмотреть запись блокировки смещенного объекта, запись блокировки в стеке и слово метки заголовка объектаилиповторное смещение к другим потокам,илиВосстановление без блокировки или маркировка объекта не подходит, так как смещенная блокировка и, наконец, пробуждает приостановленный поток.
На следующем рисунке поток 1 показывает процесс получения предвзятой блокировки, а поток 2 показывает процесс отзыва предвзятой блокировки.
Как отключить блокировку смещения
Блокировка смещения включена по умолчанию в Java 6 и Java 7, но она активируется только через несколько секунд после запуска приложения.При необходимости задержку можно отключить с помощью параметра JVM:-XX:BiasedLockingStartupDelay=0. Если вы уверены, что все блокировки в вашем приложении обычно конфликтуют, вы можете отключить предвзятую блокировку с помощью параметра JVM:-XX:-UseBiasedLocking=false, то программа по умолчанию войдет в облегченное состояние блокировки.
3.3 Легкий замок
замок
Прежде чем поток выполнит синхронизированный блок, JVM сначала поместит его в кадр стека текущего потока.Создайте место для хранения записей блокировки, и скопируйте Mark Word в заголовке объекта в запись блокировки, официально называемуюDisplaced Mark Word. Затем поток пытается использовать CASЗамените слово Mark в заголовке объекта указателем на запись блокировки.. Если это удается, текущий поток получает блокировку, если это не удается, это означает, что другие потоки конкурируют за блокировку, и текущий поток пытается использовать вращение, чтобы получить блокировку.
разблокировать
При облегченной разблокировке используется атомарная операция CAS для замены слова Displaced Mark Word обратно на заголовок объекта.В случае успеха конкуренция не возникает. Если это не удается, это означает, что текущая блокировка конкурирует, и блокировка превратится в тяжеловесную блокировку. На следующем рисунке показана блок-схема двух потоков, одновременно конкурирующих за блокировки, что приводит к раздуванию блокировок.
Поскольку вращение потребляет ресурсы ЦП, чтобы избежать бесполезного вращения (например, когда поток, получающий блокировку, блокируется), после того, как блокировка будет обновлена до тяжеловесной блокировки, она не вернется в состояние облегченной блокировки. Когда блокировка находится в этом состоянии, когда другие потоки пытаются получить блокировку, все они будут заблокированы.Когда поток, удерживающий блокировку, освобождает блокировку, эти потоки будут пробуждены, и пробужденный поток получит новый раунд блокировки. соревнование.
3.5 Сравнение различных замков
4. Пример
После вышеупомянутого понимания мы должны знать, как решить. Исправленный код:
public class SynchronizedDemo implements Runnable {
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new SynchronizedDemo());
thread.start();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + count);
}
@Override
public void run() {
synchronized (SynchronizedDemo.class) {
for (int i = 0; i < 1000000; i++)
count++;
}
}
}
Откройте десять потоков, каждый поток накапливает исходное значение 1000000 раз, и окончательный правильный результат равен 10X1000000 = 10000000. Здесь можно рассчитать правильный результат, поскольку в операции накопления используется блок кода синхронизации, что гарантирует, что все значения из общих переменных, полученных каждым потоком, являются последними текущими значениями.Если синхронизация не используется, поток A может накапливать, а поток B может использовать исходное значение для операции накопления, то есть «грязное значение». Таким образом, окончательный результат расчета неверен. С Syncnized можно гарантировать видимость памяти, гарантируя, что каждый поток является последним значением операции. Это просто образцовая демонстрация, умник, есть ли другой выход?
использованная литература
Искусство параллельного программирования на Java