В программировании на Java есть очень неприятная проблема, которой хочет избежать каждый программист, а именно проблема безопасности многопоточности. Эта часть знаний должна быть одной из самых проблемных в Java.Сегодня я расскажу о том, как решать проблемы безопасности потоков в Java и о разнице между различными блокировками.
Основная причина проблем с безопасностью потоков
1. Наличие общих данных (критические ресурсы)
2. Существует несколько потоков для совместной работы с данными.
Решать проблемуФундаментальный метод: существует только один поток, работающий с общими данными одновременно, и другие потоки должны ждать, пока поток завершит обработку данных, прежде чем работать с общими данными.
Synchronized
Особенности мьютекса
взаимная исключительность: то есть только одному потоку разрешено одновременно удерживать объектную блокировку, и эта функция используется для реализации многопоточного механизма координации, так что только один поток может получить доступ к блоку кода (составной операции), который необходимо синхронизироваться одновременно. Взаимное исключение также известно как атомарность операций. видимость: До снятия блокировки необходимо убедиться, что изменения, внесенные в общую переменную, видны другому потоку, который впоследствии получает блокировку (т. е. самое последнее значение общей переменной должно быть получено при получении блокировки), в противном случае другой поток может быть Продолжить операцию с копией локального кеша, что приведет к несогласованности.
Для Java ключевое слово Synchronized может удовлетворять двум вышеуказанным характеристикам. Примечание: Синхронизированные замки не кодовые, аобъект.
Синхронизированная классификация захвата замков
блокировка объекта
Есть два способа получить блокировку объекта:
1.Блок синхронизированного кода (синхронизированный (этот), синхронизированный (объект экземпляра класса)), замок — это объект в круглых скобках.
2.Синхронизированный нестатический метод (синхронизированный метод), блокировка является экземпляром текущего объекта.
блокировка класса
Есть два способа получить блокировку класса:
1.Блок синхронизированного кода (синхронизированный (object.getClass()), синхронизированный (имя класса.класс)), замок — это объект класса (class object) объекта, заключенного в круглые скобки.
2.синхронизированный статический метод, блокировка является объектом класса текущего объекта.
public class ThreadTest {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
Thread thread1 = new Thread(threadDemo,"ASYN_Thread1");
Thread thread2 = new Thread(threadDemo,"ASYN_Thread2");
Thread thread3 = new Thread(threadDemo,"SYNC_METHOD_Thread1");
Thread thread4 = new Thread(threadDemo,"SYNC_METHOD_Thread2");
Thread thread5 = new Thread(threadDemo,"SYNC_BLOCK_Thread1");
Thread thread6 = new Thread(threadDemo,"SYNC_BLOCK_Thread2");
Thread thread7 = new Thread(threadDemo,"SYNC_STATIC_Thread1");
Thread thread8 = new Thread(threadDemo,"SYNC_STATIC_Thread2");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
thread6.start();
thread7.start();
thread8.start();
}
}
Результаты теста
Это также видно из приведенного выше теста: один и тот же классМежду блокировками объекта класса и блокировкой объекта экземпляра нет корреляции..
Обзор блокировок объектов и блокировок классов
1. Когда поток обращается к блоку синхронизированного кода объекта, другой поток может получить доступ к блоку синхронизированного кода объекта.Асинхронный блок кода.
2. Если блокировка является тем же объектом, поток обращается к объектуБлок синхронизированного кодакогда другой обращается к объектуБлок синхронизированного кодаТема будет заблокирована.
3. Если блокировка является тем же объектом, поток обращается к объекту.Синхронный методкогда другой объект доступаСинхронный методТема будет заблокирована.
4. Если блокировка является тем же объектом, поток обращается к объекту.Блок синхронизированного кодакогда другой объект доступаСинхронный методТема будет заблокирована, и наоборот.
5. Объектные блокировки разных объектов одного класса не мешают друг другу.
6. Поскольку классовая блокировка также является специальной блокировкой объекта, ее эффективность согласуется с указанными выше 1, 2, 3 и 4. Поскольку класс имеет только одну блокировку объекта, использование блокировок класса разными объектами одного и того же класса будет синхронным.
7. Блокировки классов и блокировки объектов не мешают друг другу.
Реализация основного принципа Synchronized
После краткого понимания Synchronized мы углубимся в основные принципы Synchronized.
Основы реализации Synchronized
Заголовок объекта Java
В виртуальной машине Hotspot расположение объектов в памяти разделено на следующие три части:
1.заголовок объекта 2. Данные экземпляра 3. Выровняйте отступы Так как я не очень разбираюсь в данных инстанса и его заполнении, и у меня не очень хорошее отношение к этой части, я пока не буду упоминать об этом.Расскажите подробно о голове объекта: Заголовок объекта разделен на две части:
1.Mark Word: Хэш-код, возраст поколения, тип блокировки, флаг блокировки и другая информация об объекте сохраняются по умолчанию.
2.Class Metadata Address: Указатель типа указывает на метаданные класса объекта, и JVM использует этот указатель, чтобы определить, к какому классу относится объект. Поскольку информация заголовка объекта является дополнительными данными, не имеющими никакого отношения к работе объекта, учитывая проблему эффективности работы, MarkWord разработан как нефиксированная структура данных, чтобы хранить больше достоверных данных, он будет в зависимости от состояния самого объекта повторно используйте собственное пространство для хранения.
Монитор (Блокировка монитора)
С момента своего рождения объекты Java инкапсулировали внутри невидимый замок —Модитор (блокировка монитора), мы можем использовать его как инструмент синхронизации. Существуют различные отношения между монитором и объектами, например, он может быть сгенерирован вместе с объектом или может быть сгенерирован автоматически, когда объекту необходимо получить блокировку. Конструктор Monitor (C++) выглядит следующим образом:
Сначала запомните эти свойства (сверху вниз):считать (счетчик),object,владелец (поток, в настоящее время удерживающий блокировку),WaitSet (пул ожидания),_EntryList (пул блокировки).
Как видно из исходного кода, в Мониторе две очереди, однаWaitSet, другой _EntryList, две очередипул ожиданияа такжезамок пул, когда несколько потоков хотят получить блокировку, они входят вместеЗаблокировать пул (EntryList), только один поток может получить блокировку, а другие потоки должны ожидать в пуле блокировок, и поток, который успешно получит блокировку, войдет в _objectобласть и поместите _ в мониторownerАтрибут меняется на текущий поток, а счетчик в Monitor_countдобавит один. Когда поток вызывает метод wait(),ownerсвойство будет установлено в null,countОн также уменьшится на единицу, а текущий поток войдет в _WaitSetждет, когда его разбудят.
С этой точки зрения блокировка Monitor существует в заголовке объекта каждого объекта Java, а ключевое слово Synchronized получает блокировку таким образом, поэтому любой объект в Java может использоваться в качестве блокировки.
Понимание синхронизации с уровня байт-кода
Предварительная подготовка (код)
public class ThreadClassDemo {
public void syncPrint() {
//同步代码块
synchronized(this) {
System.out.println("hello——sync block");
}
}
public synchronized void syncMethodPrint() {
System.out.println("hello——sync method");
}
}
Просмотр файлов байт-кода с помощью команды javap
Найдите каталог, в котором находится файл класса, в каталоге кода Java, который вы только что написали, и используйтеjavap -verbose ThreadClassDemo.classКоманда для просмотра файла байт-кода.
Файл байт-кода с использованием синхронизированных блоков кода
Файл байт-кода с использованием синхронизированных методов:
Мы видим, что monitorenter и monitorexit явно используются в байт-коде с использованием синхронизированных блоков кода для представления блокировки и снятия блокировок, в то время как в байт-коде с использованием синхронизированных методов нет monitorenter и monitorexit. На самом деле в процессе синхронизации метода синхронизации monitorenter и monitorexit не отображаются. ТакКак достигается синхронизация?? Мы видим, что в файле байт-кода синхронизированного метода есть атрибут,flags, которая включает в себяACC_SYNCHRONIZED, этот флаг может определить, является ли метод синхронным методом.Когда метод вызывается, система определяет, имеет ли метод флаг ACC_SYNCHRONIZED, затем поток, выполняющий метод, будет удерживать монитор, затем выполнение завершается, и, наконец, монитор освобожден.
Небольшой разговор о Synchronized
Узнав о Synchronized раньше, кто-то всегда говорил, что Synchronized — это тяжеловесная блокировка. Действительно, в более ранних версиях (до Java 1.6) Synchronized была тяжеловесной блокировкой, потому что в основном полагалась на реализацию MutexLock, и при каждой блокировке требовалось переключаться из пользовательского состояния в базовое, что является проблемой для Процессор Тяжелая работа. В сценарии с высокой степенью параллелизма нереально выполнять преобразование пользовательского режима в базовый режим каждый раз при добавлении блокировки, поэтому позже я буду сравнивать ReentrantLock с Synchronized. Но сегодняшний день отличается от прошлого.После JDK1.6 производительность Synchronized значительно улучшилась, и это уже не тяжеловесная блокировка, которую презирали в то время.
Некоторые оптимизации, сделанные Hotspot на блокировках после JDK1.6
блокировка спина
Во многих случаях заблокированное состояние общих данных недолговечно, и не стоит переключать или приостанавливать потоки на это время. В сегодняшней многопроцессорной среде вполне возможно не блокировать поток, который не получил блокировку, а позволить ему подождать некоторое время, не жертвуя временем выполнения ЦП.Это поведение называетсявращаться,Прямо сейчасСделать темы ждать, циклически, не отказываясь от процессора. Эта стратегия очень эффективна, когда время занятости блокировки очень короткое, поскольку позволяет избежать частых переключений контекста. Однако, если блокировка занята другими потоками в течение длительного времени, этот циклический метод ожидания будет равномерно загружать ЦП, что приведет к большому ненужному снижению производительности.Если такая ситуация существует, следует использовать традиционный метод для прямой приостановки Неприобретенный к резьбе замка.
Адаптивная блокировка спина
Адаптивная блокировка спина, как следует из названия, количество спинов больше не фиксировано, а количество спинов определяется в зависимости от ситуации, в которой предыдущий поток текущего потока получает блокировку. Если предыдущий поток не может успешно получить блокировку, JVM будет считать, что текущий поток имеет высокую вероятность получения блокировки, и соответствующим образом увеличит количество вращений, чтобы избежать накладных расходов, вызванных переключением блокировки; если предыдущий поток не получил блокировку, тогда JVM будет думать, что вероятность того, что текущий поток получит блокировку, невелика, поэтому она завершит вращение раньше, чтобы не тратить ресурсы ЦП.
снятие блокировки
Устранение блокировок — еще одна стратегия оптимизации виртуальной машины.Во время процесса JIT-компиляции некоторые блокировки, которые, по мнению JVM, не столкнутся с конкуренцией, автоматически устраняются, то есть когда JVM определяет, что заблокированный ресурс является ресурсом, которым нельзя поделиться, снять его замок.
блокировка огрубления
public class Test{
public static void main(String[] args){
StringBuffer sb = new StringBuffer();
int i = 0;
while(i<100){
sb.append("test")
i++;
}
}
}
Например, в приведенном выше коде мы знаем, что StringBuffer является потокобезопасным, поэтому каждый раз, когда вызывается метод append, он будет пытаться заблокироваться, но в приведенном выше примере только один объект многократно блокирует и снимает блокировки, блокирует и освобождает блокировки... вызывающие частые В это время JVM автоматически расширит гранулярность блокировки за пределы цикла, то есть при входе в цикл блокировка будет добавлена, а затем блокировка не будет повторяться при добавлении выполняется операция.
Четыре состояния синхронизации
Synchronized имеет четыре состояния, которыенет замка,Блокировка смещения,Легкий замок,тяжелый замок, автоматическое обновление или понижение версии в соответствии с различными сценариями.Направление обновления:Без блокировки --> Блокировка смещения --> Легкая блокировка --> Тяжелая блокировка.
нет замка
Хорошо понятно, что нет блокировки, нет блокировки, асинхронность, гм.
Блокировка смещения
Основное назначение смещенных замков состоит в том, чтобы:Сокращение затрат на приобретение замков тем же потоком. В большинстве случаев многопоточная конкуренция за блокировку отсутствует, и она всегда повторно получается одним и тем же потоком. Чтобы решить эту проблему, хотспоты вводят смещенную блокировку во время оптимизации.Основная идея заключается в следующем: если поток получает блокировку, то блокировка находится в смещенном режиме, и структура Mark Word становитсяСтруктура блокировки смещения(Флаг блокировки равен 1 01, подробности см. в диаграмме структуры заголовка объекта выше), когда поток снова запрашивает блокировку, нет необходимости выполнять какие-либо операции синхронизации, просто проверьте Mark WordФлаг блокировки является смещенной блокировкойа такжеТекущий идентификатор потока равен идентификатору потока в Mark Word.Вот именно, что сокращает работу большого количества приложений блокировки.
Примечание. Предвзятые блокировки не подходят для многопоточных случаев с интенсивной конкуренцией блокировок.
Что касается предвзятой блокировки, я также видел объяснение, которое легче понять:
Когда объект создается впервые, потоки для доступа к нему отсутствуют. Это означает, что теперь он считает, что только один поток может получить к нему доступ, поэтому, когда первый поток обращается к нему, он отдает предпочтение этому потоку, и в этот момент объект удерживает смещенную блокировку. Смещенный к первому потоку, этот поток использует операцию CAS при изменении заголовка объекта, чтобы он стал предвзятой блокировкой, и изменяет ThreadID в заголовке объекта на свой собственный идентификатор, а затем при повторном доступе к объекту ему нужно только сравнить ID, нет необходимости использовать его снова CAS работает.
Как только второй поток получает доступ к объекту, поскольку смещенная блокировка не будет активно снята, второй поток может увидеть смещенное состояние объекта, которое указывает на то, что на этом объекте уже есть конкуренция, и проверить, что блокировка объекта была изначально удерживается. Если поток все еще жив, если он зависает, вы можете изменить объект в состояние без блокировки, а затем повторно сместить новый поток. Если исходный поток все еще жив, выполняется стек операций этого потока. немедленно, чтобы проверить использование объекта. Если смещенную блокировку все еще необходимо удерживать, смещенная блокировка обновляется до упрощенной блокировки (в настоящее время смещенная блокировка обновляется до упрощенной блокировки). Если он больше не используется, объект можно вернуть в состояние без блокировки, а затем перенаправить.
Облегченные блокировки обновляются со смещенными блокировками. Смещенная блокировка запускается, когда поток входит в синхронизированный блок. Когда второй поток вступает в соревнование замков, смещенная блокировка будет обновлена до упрощенной блокировки. (Если поток, который в настоящее время удерживает блокировку, все еще жив, он будет обновлен, в противном случае он продолжит отдавать предпочтение потоку, который в настоящее время конкурирует за блокировку).Облегченные замки подходят для случая, когда потоки выполняются попеременно и блокируются попеременно., если несколько потоков одновременно конкурируют за одну и ту же блокировку, облегченная блокировка будет заменена на тяжеловесную.
Процесс запирания облегченного замка:
1. Когда код входит в выполнение блока синхронизации, если состояние блокировки объекта синхронизации является состоянием без блокировки, виртуальная машина сначала устанавливает пространство с именем Lock Record в кадре стека текущего потока, которое используется для хранения текущего слова метки объекта блокировки.Копия, официально называемая словом Displaced Mark Word.
2. Скопируйте Mark Word из заголовка объекта в Lock Record.
3. После успешного копирования виртуальная машина будет использовать операцию CAS, чтобы попытаться обновить слово метки объекта до указателя на запись блокировки и указать указатель _owner в записи блокировки на слово метки объекта. обновление прошло успешно, перейдите к шагу 4, в противном случае перейдите к шагу 5.
4. Если операция обновления выполнена успешно, то поток владеет блокировкой объекта, а флаговый бит слова метки объекта устанавливается в 00, указывая на то, что объект находится в заблокированном состоянии облегченной блокировки.
5. Если операция обновления завершится неудачно, виртуальная машина сначала проверит, указывает ли MarkWord объекта на фрейм стека текущего потока, если да, то это означает, что текущий поток уже владеет блокировкой объекта, а затем может напрямую войти в блок синхронизации для продолжения выполнения, в противном случае это означает, что несколько потоков конкурируют за блокировки, и облегченная блокировка будет обновлена до тяжелой блокировки. Значение состояния флага блокировки становится равным 10. Указатель на тяжеловесную блокировку (мьютекс ) хранится в Mark Word, и блокировка ожидает блокировки позже.Поток также входит в заблокированное состояние, и текущий поток пытается использовать вращение для получения блокировки.
тяжелый замок
Облегченные блокировки считают, что конкуренция существует, но степень конкуренции очень легкая.Как правило, два потока будут смещать работу одного и того же замка или немного подождать (прокрутить), а другой поток освободит блокировку. Но когда вращение превышает определенное количество раз, или один поток удерживает блокировку, один крутится и происходит третье посещение, облегченная блокировка расширяется в тяжеловесную блокировку, а тяжеловесная блокировка делает все потоки, кроме потока, который владеет блокировкой.Потоки блокируются, предотвращая простаивание процессора.
Семантика памяти замков
Когда поток освобождает блокировку, модель памяти Java помещаетОбщие переменные в локальной памяти, соответствующие потоку, сбрасываются в основную память.. Когда поток получает блокировку, локальная память, соответствующая потоку, становится недействительной моделью памяти Java, так что код в критической секции, защищенной монитором, должен считывать общую переменную из основной памяти.
Синхронизированный и ReentrantLock
Здесь может быть задействован некоторый анализ исходного кода ReentrantLock, который, вероятно, требует следующих предварительных условий:
Знания, связанные с AQS: CAS, спин-блокировка, парковка, разпарковка
Среди них спин упоминается выше, CAS будет упоминаться ниже, вы можете спуститься и прочитать его, а затем оглянуться на этот абзац, парковаться, разпарковаться нужно только знать его функцию, а друзья, которые не поняли, могут это сделать по Baidu, это не имеет большого значения.
О ReentrantLock
блокировка повторного входа(мне не особо нравится этот перевод, ниже он еще называется ReentrantLock), его семантика в основном такая же, как Synchronized, основанная наAQSреализованоDougLeaОн был написан Дашеном и был представлен в версии Java 1.5.Болезнь, которую изначально хотели решить, заключалась в том, что каждый раз, когда Synchronized блокируется, необходимо выполнять утомительное переключение пользовательского режима на режим ядра (до Java 1.6), что приводит к трата ресурсов. И блокировка ReentrantLock находится вАльтернативное выполнение потоковВ этом сценарии это можно сделать полностью на уровне JVM, без необходимости вызывать метод Native, а затем выполнять системный вызов каждый раз, когда добавляется блокировка, например Synchronized, что позволяет избежать частого переключения состояния пользователя и ядра и повышает скорость.
Процесс блокировки ReentrantLock
Чтобы понять этот процесс, это будет немного долго и немного сложно, и некоторые люди говорят, что я не должен быть нетерпеливым, когда я впервые что-то узнал, и начать писать параллелизм и другой контент, но я все еще надеюсь, что я могу вывести некоторые исходный код, я буду использовать свой собственный способ, чтобы рассказать этот отрывок с наиболее объективной и практической отправной точки.Я надеюсь, что каждый прочитает этот отрывок в духе взаимного обучения. Так что, если где-то есть ошибка, это все еще старые правила, вы можете просто связаться со мной в области комментариев или в личном чате со мной.
Я предполагаю, что для получения блокировки есть три потока A, B и C. Исходя из этого сценария, я расскажу о процессе блокировки ReentrantLock (весь процесс я напишу в комментариях к коду, а там будет быть некоторые прыжки в середине. место, чтобы посмотреть).
Поскольку ReentrantLock реализован на основе AQS (ReentrantLock использует внутреннюю синхронизацию, которая является подклассом AQS.), поэтому вы должны сначала понять, как выглядит структура AQS.
ReentrantLock честный и несправедливый
public ReentrantLock(boolean fair) {
//构造方法
//如果传入的是true,则创建一个公平锁,如果传入false,创建一个非公平锁
//默认是非公平锁
//非公平锁和公平锁,都继承自ReentrantLock的静态内部类Sync,Sync继承自AQS
sync = fair ? new FairSync() : new NonfairSync();
}
Блокировки потока (сначала поговорим о процессе блокировки честных блокировок)
1. ПервыйТемаПопытка получить блокировку (вызовите метод reentrantLock.lock())
public final void acquire(int arg) {
/*首先调用tryAcquire方法尝试获取锁,为了方便,我直接把tryAcquire方法贴在下方
*(看到这里的小伙伴直接跳到下方代码tryAcquire方法代码
*/
/*==================================分割线==================================*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
/*根据下方代码,tryAcquire方法返回结果:true,这个if语句中将true取反,变为false,
*于是这个if语句不会被执行,acquire方法直接返回。
*/
}
//从acquire方法进入tryAcquire方法
protected final boolean tryAcquire(int acquires) {
//获取当前线程:线程A
final Thread current = Thread.currentThread();
//获取当前state状态,state是锁是否被占用的标志位,0表示自由,1表示被占用
//当前只有A线程开始尝试获取锁,那么锁肯定是自由的,state == 0,即c == 0、
int c = getState();
if (c == 0) {
//此时c一定等于0,于是到这里
//有一个hasQueuedPredecessors方法,这个方法是判断是否等待队列中有排在自己之前的元素。源代码我也贴在下方。(看到这的同学直接跳到第4点看hasQueuedPredecessors方法)
/*==================================分割线==================================*/
//根据下方第4点结果说明,hasQueuePredecessors返回false,这个if语句对这个结果取反,就变成了true。
//可以继续执行下一个条件:campareAndSetState(0,acquires),即CAS。
//CAS操作将state由0改为1,说明锁此时被占用,那么被谁占用了呢?
//执行setExclusiveOwnerThread(current),将锁的持有者改为当前线程。
//整个方法返回 true。
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
4. Введите метод hasQueuedPredecessors
public final boolean hasQueuedPredecessors() {
//将AQS的队尾元素赋值给t
Node t = tail;
//将AQS的队头元素赋值给h
Node h = head;
Node s;
/*A线程会进到这里,首先判断h是不是等于t
*线程A是第一个到达这里的,此时队列并没有被初始化,所以h == null,t==null,
*所以此时h != t为false,这个方法直接返回false
*/
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
С помощью вышеуказанных 4 шагов вы можете напрямую определить процесс блокировки потока A, а также процесс блокировки потока A.Когда во всей очереди AQS нет ни одного элементаПроцесс блокировки , иКак блокировка успешна?, на самом деле метод блокировки возвращается нормально, то есть блокировка прошла успешно, потому что, как вы увидите позже, если блокировка не удалась, метод блокировки не вернется в нормальном режиме.
Процесс блокировки потока B
Старое правило заключается в том, чтобы сначала опубликовать код.нить БВведите метод блокировки, чтобы попытаться удержать блокировку. 1. Поток B входит в метод блокировки
final void lock() {
/*公平锁的lock()方法实际上封装的是acquire方法,而非公平锁是直接进行CAS操作尝试获取锁
如果获取锁失败,调用acquire方法。
*/
acquire(1);
}
2. Поток B входит в метод получения
public final void acquire(int arg) {
/*线程B首先调用tryAcquire方法尝试获取锁
*(看到这里的小伙伴直接跳到3,tryAcquire方法)
/*==================================分割线==================================*/
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
/*根据下方代码,tryAcquire方法返回结果:false,这个if语句中将false取反,变为true,
*于是这个if语句会继续往下判断,调用acquireQueue方法,而调用acquireQueue方法之前,会
*先调用addWaiter方法,看到这的同学跳到4,进入addWaiter方法
*/
/*==================================分割线==================================*/
/*根据4的结果,addWaiter方法返回了一个保存当前线程,即线程B的Node对象*/
/*==================================分割线==================================*/
/*于是继续执行acquireQueued方法,跳到6*/
}
3. Поток B входит в метод tryAcquire
//线程B从acquire方法进入tryAcquire方法
protected final boolean tryAcquire(int acquires) {
//获取当前线程:线程B
final Thread current = Thread.currentThread();
//获取当前state状态,state是锁是否被占用的标志位,0表示自由,1表示被占用
//当前线程A正在持有锁,所以state==1,即c==1
int c = getState();
if (c == 0) {
//此时c==1,于是不能到达这里
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//判断当前线程是否为持有锁的线程
//显然线程B不是当前持有锁的线程,于是也到不了这里。
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//线程B整个tryAcquire方法返回false
return false;
}
final boolean acquireQueued(final Node node, int arg) {
//该方法传入一个node,即保存了当前线程的node,当前线程即线程C
//arg = 1
boolean failed = true;
try {
boolean interrupted = false;
//死循环
for (;;) {
//首先将当前线程的node的前一个node 赋值给p,即p==保存了线程B的Node
final Node p = node.predecessor();
//p此时在队列中排第二个位置,所以它不是头部,不会进入该判断。
/*
*这里解释一下它的用意,当一个线程的前一个线程是队首,那么它在进入这个方法时,
*有可能队首的线程已经执行完毕并将锁释放
*那么可以去尝试获取一下锁
*但是如果你的前一个Node都不是队首,说明你的前面还有人在排队
*所以你就不用再去尝试获取锁了,因为还轮不到你。
*所以直接放弃执行这段代码,乖乖去排队吧。
*/
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//于是进入这个判断,直接判断是否需要park
//流程和刚才线程B执行shouldParkAfterFailedAcquire方法是一样的
//所以我就不重新贴代码了。
//最终线程C会在这里被park住
//方法结束,但是不会正常返回,线程C就直接阻塞在这了,等待前面的线程执行完来唤醒他吧。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
Краткое описание процесса блокировки
Первый поток получает блокировку
Когда блокировка простаивает, первый поток пытается получить блокировку, что является ситуацией, в которой поток A получает блокировку, описанную выше. В это время очередь также не инициализирована, фактически после того, как поток А успешно получит блокировку, очередь также не будет инициализирована. Можно видеть, что весь процесс получения блокировки потоком А проходит очень гладко, не используются системные вызовы, не выполняется преобразование пользовательского режима в режим ядра, и все операции полностью выполняются на Java. Итак, если нитьАльтернативное исполнение, то очень эффективно использовать ReentrantLock как средство синхронизации. Поскольку нет блокировки потока, нет необходимости делать системный вызов для блокировки.
Второй поток получает блокировку
Когда блокировка уже занята, второй поток пытается получить блокировку, что может соответствовать сценарию, в котором поток B получает блокировку выше. На данный момент очередь не инициализирована, поэтому поток B перейдет кИнициализировать всю очередь, но в это время в очереди находится не только поток B, но и узел перед потоком B. Хотя атрибут потока этого узла равен нулю, мы можем рассматривать его как то, что он представляетпоток A в настоящее время держит блокировку. Поскольку поток A не участвовал в постановке в очередь от начала до конца, свойство потока первого Node будет равно null; или можно понимать так:В очереди первый человек, стоящий в очереди, не стоит в очереди, а занимается бизнесом, и тогда второй человек или более поздние могут рассматриваться как стоящие в очереди.. Сказав так много, я просто хочу показать, что сейчас в очереди два узла. Затем, когда B завершит создание узла, поскольку поток B является вторым в очереди, его очередь наступит после выполнения A, поэтому в это время поток B проверит, завершил ли поток A выполнение и снял блокировку. получить замок, которыйпервый спин. еслиПопытка получить замок не удалось, на этом все закончилось, вот-вот будет парковаться, так что давайте подчищаем и меняем waitStatus предыдущего узла на Node.SIGNAL, что означает, что он готов к блокировке, но перед паркингом поток B перезапустится поставит умирающая борьба,Что если! Что, если замок можно приобрести сейчас? ! ! !, поэтому он снова пытается получить блокировку, и если это снова не удается, он напрямую блокируется парком.
Третий поток получает блокировку
Когда блокировка занята и очередь инициализирована, перед очередью также стоят потоки. Это ситуация, с которой столкнулся поток C. В это время поток C также попытается получить блокировку, он должен быть не в состоянии получить ее, потому что впереди все еще есть очереди, и сейчас не ваша очередь. Так что он сразу стал ждать в очереди. Ожидая в очереди, он смотрел на свой статус. В-третьих, не пытайтесь получить замок. Перед ним все еще куча людей. припарковался на месте.но, третье место не означает потерю надежды! Он дважды прокрутится, чтобы увидеть, есть ли у него шанс перейти на вторую позицию, и если он находится во второй позиции, он все равно попытается получить блокировку.
Окончательная диаграмма очереди
Суть замка
На самом деле все очень просто: поток Lock, который успешно получил блокировку, может нормально вернуться, и после нормального возврата код после Lock может продолжать выполняться. Потоки, которые не получили блокировку, блокируются в блокировке и не могут продолжать выполняться.
Яма осталась в процессе блокировки
Почему метод parkAndCheckInterrupt() возвращает Thread.interrupted() вместо прямого возврата true или false или не возвращает его напрямую?
Сначала перейдите к исходному коду parkAndCheckInterrupt:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用park()使线程进入waiting状态
return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}
Когда эта функция выполняется для метода park, поток был заблокирован, но после того, как текущий поток снимает блокировку, он разбудит текущий заблокированный поток, поэтому поток продолжит выполнение? Конечно, это не так просто, для начала нужно определить, был ли поток прерван пользователем, потому что некоторые разработчики будут добавлять в код подобную логику:Если вы не можете получить блокировку после ожидания x секунд, вам больше не нужно выполнять, просто прервите, то после пробуждения потока он фактически находится в прерванном состоянии.
Тогда возникает другой вопрос, поток, который получает блокировку через метод lock(), если блокировка занята и поток заблокирован, если вызывается метод interrupt() заблокированного потока, будет ли отменено получение блокировки ? ответотрицательный. LockSupport.park будет реагировать на прерывание, но не будет генерировать InterruptedException() (в это время можно вызвать метод lockInterruptably() для блокировки, если поток будет прерван, будет брошено InterruptedException()). То есть, если мы используем метод блокировки для блокировки, а затем используем прерывание для прерывания потока, он не получит никакого ответа, так зачем возвращать статус прерывания в parkAndCheckInterrupt? Плохо ли возвращать void напрямую?Давайте посмотрим на логику метода lockInterruptably().:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
//如果线程已经中断,则直接抛出异常
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//这里和普通的lock加锁不一样了。
//lock加锁只是将interrupted状态记录下来
//interrupted=true;
//而这里会直接抛出异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
Итак, почему метод parkAndCheckInterrupt() возвращает Thread.interrupted() вместо void? На самом деле, это потому, что parkAndCheckInterrupt также нужно вызывать в doAcquireInterruptently, чтобы логику кода в этом методе можно было повторно использовать в блокировке блокировки и lockInterruptably, чтобы возвращалось состояние прерванного.
Оставьте мысль,selfInterruptКакова роль(), зачем нужно снова вызывать этот метод и какие будут последствия, если вы его не вызовете? Читатели приглашаются к обсуждению в разделе комментариев.
Небольшой разговор о Synchronized и ReentrantLock
До Java 1.6 Synchronized был бескомпромиссной тяжеловесной блокировкой.Каждый процесс блокировки требовал переключения между режимом пользователя и режимом ядра и блокировал потоки, которые не получали блокировку.Запущенный в то время ReentrantLock выжил, чтобы решить эту проблему. Но теперь Synchronized провел большую оптимизацию после Java 1.6, представил Spin, Adaptive Spin и разделил Synchronzied на четыре состояния: без блокировки, предвзятая блокировка, облегченная блокировка, усиленная блокировка, обновление блокировки и понижение блокировки автоматически выполняются в разных сценариях. , так что теперь Synchronized может избавиться от своей блокировки веса, и даже его эффективность может превзойти ReentrantLock в некоторых сценариях.ReentrantLock все еще необходим?, очевидно, да, ReentrantLock предоставляет богатый API, он более гибкий в использовании и обеспечивает реализацию справедливых и нечестных блокировок, чего не может Synchronized.
Видимость памяти JMM
Модель памяти Java JMM (модель памяти Java, отличная от JVM)
Модель памяти Java сама по себе является абстрактным понятием и на самом деле не существует.Она описывает набор правил или спецификаций, посредством которых различные переменные в программе (включая поля экземпляра, статические поля и элементы, составляющие объект массива).Это не совсем то же самое, что и концепция модели памяти Java JVM. Вы можете посмотреть мой предыдущий блог. Модель памяти JVM Java написана на ней. Из-за того же имени, но разных концепций концепции модели памяти в этой статье упоминаются вместе в качестве JMM Модель памяти Java JVM также называется моделью памяти Java., JMM и разделение памяти Java - это разные концептуальные уровни. JMM описывает набор правил, которые вращаются вокруг атомарности, видимости и упорядоченности. Единственное сходство между ним и моделью памяти Java заключается в том, чтоКак общие, так и частные зоны, общая область JMM — это основная память, в то время как общая область модели памяти Java включает в себя область кучи и область методов; частная область JMM — это рабочая память, а частная область модели памяти Java включает счетчик программ, стек виртуальной машины Java, стек локальных методов.
JVM создаст рабочую память (пространство стека) для каждого потока для хранения личных данных потока, в то время как Java предусматривает, что все переменные существуют в основной памяти, основная память является общей областью памяти, к которой могут обращаться все потоки, но поток Операции над переменными должны выполняться в рабочей памяти. Таким образом, каждый поток сначала копирует переменную из основной памяти в рабочую память, а затем записывает переменную обратно в основную память после завершения операции.
оперативная память JMM
Основная память в основном хранит объекты экземпляра Java, в том числеПеременные-члены, информация о классе, константы, статические переменныеЖдать. Поскольку он принадлежит к общей области, это вызовет проблемы с безопасностью потоков при многопоточных параллельных операциях.
оперативная память JMM
Рабочая память в основном хранит все текущие методыИнформация о локальной переменной, индикатор номера строки байт-кода, информация о собственном методе, локальные переменные не видны другим потокам. Фактически рабочая память хранит копию копии переменной в основной памяти, и каждый поток может обращаться только к своей локальной памяти. Поскольку рабочая память является частным пространством, проблем с безопасностью потоков не возникает.
Кратко описаны типы хранения данных в основной и рабочей памяти, а также методы работы с ними.
1. в методебазовый тип данныхСтруктура кадра стека локальных переменных хранится непосредственно в рабочей памяти.
2.тип ссылкилокальные переменные, ссылочные объекты хранятся во фрейме стека, а объекты-экземпляры хранятся в основной памяти.
3.переменные-члены экземпляра объекта,статическая переменная,информация о классехранятся в основной памяти.
4.Как распределяется основная памятьКаждый поток копирует переменную в рабочую память и обновляет ее обратно в основную память после завершения операции.
Как JMM решает проблему видимости памяти
Условия, которые необходимо выполнить для переупорядочивания инструкций
1. Результат выполнения программы нельзя изменить в однопоточной среде.
2. Изменение порядка не допускается, если есть зависимости данных.
То есть переупорядочение инструкций может быть выполнено только в том случае, если оно не может быть выведено по принципу «происходит до».
происходит до принципа
Результат операции A должен быть виден операции B, тогда между A и B возникает связь «происходит до».,Например:
Восемь принципов того, что происходит раньше (я тоже выглядел сбитым с толку):
1. Принцип порядка программы: внутри потока, в соответствии с порядком кода, операции, написанные впереди, выполняются раньше, чем операции, написанные сзади.
2. Правила блокировки: сначала выполняется операция разблокировки перед последующей операцией блокировки для той же блокировки.
3. Правило volatile-переменной: операция записи в переменную происходит раньше, чем последующая операция чтения переменной.
4. Правило доставки: если операция A происходит первой в операции B, а операция B происходит первой в операции C, то можно сделать вывод, что операция A происходит первой в операции C.
5. Правила запуска потока: метод start() объекта Thread выполняется первым для каждого действия этого потока.
6. Правила прерывания потока: вызов метода прерывания потока () происходит первым, когда код терминального потока обнаруживает возникновение события прерывания.
7. Правила завершения потока: все операции в потоке происходят первыми при обнаружении завершения потока.Мы можем завершить его с помощью метода Thread.join(), а возвращаемое значение Thread.isAlive() определяет, что поток завершил выполнение.
8. Правила финализации объекта. Инициализация объекта происходит сначала в начале его метода finalize().
Если две операции не удовлетворяют ни одному из приведенных выше правил «происходит до», то порядок этих двух операций не гарантируется, и JVM может переупорядочить эти две операции;Если операция А происходит раньше операции Б, то операция А совершается над операцией памяти Б..
Видимость изменчивого
volatile: это облегченный механизм синхронизации, предоставляемый JVM. он можетУбедитесь, что оформленная общая переменная всегда видна всем потокам.,Достаточно хорошоОтключить переупорядочивание инструкций, но Volatile в многопоточной средеБезопасность не гарантируется.
public class VolatileTest{
public static volatile int value = 0;
public static void main(String[] args){
inc();
}
public static void inc(){
value++;
}
}
Значение изменяемого ключевого слова volatile будет обновляться в основной памяти сразу же при каждом его выполнении, но в многопоточной среде возникнут проблемы с безопасностью, поскольку операция value++ не является атомарной, вы можете использовать синхронизированную модификацию метод обеспечения его безопасности.
Почему volatile-модифицированные переменные могут гарантировать видимость?
когданаписать изменчивую переменную, JMM обновит значение общей переменной в рабочей памяти, соответствующей потоку, в основную память.
когдапрочитать изменчивую переменную, JMM аннулирует рабочую память, соответствующую потоку, и заставит ее читать из основной памяти.
Как volatile предотвращает изменение порядка
Барьер памяти: Гарантировать порядок выполнения определенных операций и гарантировать видимость в памяти определенных переменных. Volatile может предотвратить оптимизацию переупорядочения инструкций до и после барьера памяти, вставив инструкцию барьера памяти. Затем он принудительно сбрасывает данные кеша различных ЦП, поэтому любой поток на любом ЦП может прочитать последнюю версию этой переменной.
Разница между volatile и синхронизированным
1. Суть volatile в том, чтобы сообщить JVM, что значение текущей переменной в регистре (рабочей памяти) неопределенно и его нужно считать из памяти, synchronized — в блокировке текущей переменной, только текущий поток может получить доступ к переменная, а другие потоки блокируются до тех пор, пока поток не завершит операцию с переменной.
2.volatile можно использовать только на уровне переменной, а synchronized можно использовать на уровне метода, переменной и класса.
3. volatile может обеспечить только видимость модификации переменных и не гарантирует атомарность. А синхронизация может гарантировать видимость и атомарность модификации переменных.
4.volatile не приведет к блокировке потока, синхронизация может привести к блокировке потока.
5. Переменные, помеченные как volatile, не будут оптимизированы компилятором, а переменные, помеченные как synchronized, могут быть оптимизированы компилятором.
CAS
CAS (CompareAndSwap) — эффективный способ обеспечения безопасности потоков.
1. Он поддерживает операции атомарного обновления, подходящие для счетчиков, секвенсоров и других сценариев.
2. Принадлежатоптимистическая блокировкамеханизм.
3. В случае сбоя операции CAS разработчик решает, продолжать ли попытки или выполнить другие операции.
CAS думал
CAS содержит три операнда, а именноV (ячейка памяти), E (старое значение), N (новое значение), если и только еслиV==Eвремя, можетИзменить V на N, в противном случае разработчик должен повторить попытку или выполнить другие операции.Фактически, серия классов Atomic в JUC использует CAS для обеспечения атомарной работы переменных.. Во многих случаях разработчикам не нужно напрямую использовать CAS для решения проблем безопасности потоков, а нужно напрямую использовать пакет JUC для обеспечения безопасности потоков.
Недостатки CAS
1. Если вы решите повторить попытку после сбоя, если время цикла будет большим, это окажет большее влияние на производительность.
2. Гарантируется только атомарная операция общей переменной.
3.АВА-проблема, Предположим, что поток считывает значение переменной A, и значение переменной по-прежнему равно A, когда оно готово к назначению. Можно ли гарантировать, что значение этой переменной не было изменено другими потоками? Что, если значение этой переменной сначала изменится на B другим потоком, а затем изменится на A другим потоком во время изменения значения?решить: AtomicStampedReference, представляет номер версии.
оптимистическая блокировка и пессимистическая блокировка
Наконец, давайте поговорим об относительно расслабленном содержании, оптимистичной блокировке и пессимистичной блокировке, давайте представим концепцию.
оптимистическая блокировка
Всегда предполагайте наилучшую ситуацию.Каждый раз, когда я иду за данными, я думаю, что другие не изменят их, поэтому они не будут заблокированы.Однако при обновлении он будет судить, обновляли ли другие данные в течение этого периода.Вы можно использоватьМеханизм номера версии и алгоритм CASвыполнить. Оптимистическая блокировка подходит для типов приложений с множественным чтением, что может повысить пропускную способность.Как и механизм write_condition, предоставляемый базой данных, на самом деле это оптимистическая блокировка. Класс атомарной переменной в пакете java.util.concurrent.atomic в Java — это реализация, использующая оптимистическую блокировку ——CASосуществленный.
пессимистический замок
Всегда предполагайте наихудший случай, каждый раз, когда вы идете за данными, вы думаете, что другие изменят их, поэтому каждый раз, когда вы получаете данные, вы блокируете их, чтобы другие, которые хотят получить данные, блокировались до тех пор, пока они не будут получены. блокировка ** (общие ресурсы используются только одним потоком за раз, другие потоки блокируются, а ресурсы передаются другим потокам после использования) **. Многие такие механизмы блокировки используются в традиционных реляционных базах данных, например блокировки строк, таблиц, чтения, записи и т. д., все из которых блокируются перед выполнением операций. Эксклюзивные блокировки, такие как synchronized и ReentrantLock в Java, являются реализацией идеи пессимистической блокировки.
Два сценария использования блокировки
Оптимистическая блокировка подходит для ситуаций, когда операций записи мало (сценарии многократного чтения), то есть когда конфликты возникают действительно редко, что экономит накладные расходы на блокировки и увеличивает общую пропускную способность системы. Однако, если это ситуация с множественной записью, обычно возникают конфликты, из-за которых приложение верхнего уровня будет продолжать повторять попытки, что снизит производительность. сценарии.
Эпилог
Изначально я хотел написать эту статью очень давно. Блокировка всегда была очень сложной вещью. Я всегда чувствовал, что написать не так уж и много, поэтому я продолжал добавлять контент во время обучения; Просто выходите и пишите. Продолжаем говорить, что ведение блога на самом деле для создания собственной системы знаний, а не для того, чтобы вас чему-то научить, в конце концов, я все еще студент. Так что я надеюсь, что вы придете в этот блог с менталитетом обсуждения и обучения друг у друга.Если есть какая-то ошибка, вы можете прямо поднять ее и позволить нам обсудить и учиться вместе.
Название этого "Замок, в качестве резюме того, что я узнал о параллельном программировании за этот период времени.
Наконец, я желаю всем счастливого национального праздника и хорошо провести время.
Фотографии в этой статье взяты из Интернета, захвачены и удалены.