Java-блокировка, о которой нужно сказать

Java внешний интерфейс исходный код Операционная система

предисловие

Java предоставляет множество блокировок, каждая из которых может показать очень высокую эффективность в соответствующих сценариях благодаря своим различным характеристикам. Эта статья призвана привести примеры исходного кода, связанного с блокировками (исходный код в этой статье взят из JDK 8) и сценариев использования, чтобы познакомить читателей с базовыми знаниями об основных блокировках и применимых сценариях различных блокировок.

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

图片1

1. Оптимистичная блокировка против пессимистичной блокировки

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

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

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

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

图片1

В соответствии с приведенным выше описанием концепции мы можем найти:

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

Концепция немного абстрактна, давайте рассмотрим пример того, как называются оптимистичные и пессимистичные блокировки:

// ------------------------- 悲观锁的调用方式 -------------------------
// synchronized
public synchronized void testMethod() {
	// 操作同步资源
}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 需要保证多个线程使用的是同一个锁
public void modifyPublicResources() {
	lock.lock();
	// 操作同步资源
	lock.unlock();
}

// ------------------------- 乐观锁的调用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger();  // 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1

На примере вызова методов мы можем обнаружить, что пессимистичные блокировки в основном управляют ресурсами синхронизации после явной блокировки, а оптимистичные блокировки непосредственно управляют ресурсами синхронизации. Итак, почему оптимистическая блокировка может правильно обеспечить синхронизацию потоков без блокировки ресурсов синхронизации? Мы проясним путаницу, представив технический принцип «CAS», основной метод реализации оптимистической блокировки.

CAS расшифровывается как Compare And Swap, алгоритм без блокировки. Синхронизируйте переменные между несколькими потоками без использования блокировок (никакие потоки не блокируются). Атомарный класс в пакете java.util.concurrent реализует оптимистическую блокировку через CAS.

Алгоритм CAS включает три операнда:

  • Значение памяти V, которое необходимо прочитать и записать.
  • Значение A для сравнения.
  • Новое значение B для записи.

CAS атомарно обновляет значение V новым значением B тогда и только тогда, когда значение V равно A ("сравнение+обновление" в целом является атомарной операцией), в противном случае ничего не делается. В общем, «обновление» — это операция, которая постоянно повторяется.

Как упоминалось ранее, атомарный класс в пакете java.util.concurrent реализует оптимистическую блокировку через CAS, затем мы вводим исходный код атомарного класса AtomicInteger и смотрим на определение AtomicInteger:

图片2

Согласно определению, мы можем видеть роль каждого атрибута:

  • небезопасно: получать и обрабатывать данные в памяти.
  • valueOffset: смещение сохраненного значения в AtomicInteger.
  • value: хранит значение int AtomicInteger. Это свойство должно использовать ключевое слово volatile, чтобы убедиться, что оно видимо между потоками.

Далее, когда мы смотрим на исходный код функции автоинкремента incrementAndGet() для AtomicInteger, мы обнаруживаем, что нижний уровень функции автоинкремента вызывает unsafe.getAndAddInt(). Однако, поскольку сам JDK имеет только Unsafe.class и только через имена параметров в файле класса, мы не можем хорошо понять функцию метода, поэтому мы используем OpenJDK 8 для просмотра исходного кода Unsafe:

// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
      var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {
       v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}

Согласно исходному коду OpenJDK 8 мы видим, что цикл getAndAddInt() получает значение v по смещению в заданном объекте o, а затем оценивает, равно ли значение памяти v. Если оно равно, установите значение памяти равным v + delta, в противном случае верните false, продолжите цикл, чтобы повторить попытку, и выйдите из цикла, пока настройка не будет успешной, и верните старое значение. Вся операция «сравнение + обновление» инкапсулирована в compareAndSwapInt(), которая выполняется с помощью инструкции ЦП в JNI, которая является атомарной операцией, которая гарантирует, что несколько потоков могут видеть измененное значение одной и той же переменной.

Последующий JDK использует инструкцию ЦП cmpxchg для сравнения A в регистре со значением V в памяти. Если они равны, сохраните новое значение B для записи в память. Если они не равны, значение памяти V присваивается значению A в регистре. Затем снова вызовите инструкцию cmpxchg через цикл while в коде Java, чтобы повторить попытку, пока настройка не будет успешной.

Хотя CAS очень эффективен, у него также есть три основные проблемы, которые кратко упомянуты здесь:

1.АВА-проблема. CAS необходимо проверить, изменилось ли значение памяти при работе со значением, и значение памяти будет обновлено, если изменений нет. Но если значение памяти изначально было A, потом стало B, а потом снова стало A, то когда CAS его проверит, то обнаружит, что значение не изменилось, а на самом деле изменилось. Решение проблемы ABA состоит в том, чтобы добавить номер версии перед переменной и добавлять единицу к номеру версии каждый раз, когда переменная обновляется, чтобы процесс изменения менялся с «ABA» на «1A-2B-3A». .

Начиная с версии 1.5 JDK предоставляет класс AtomicStampedReference для решения проблемы ABA, а конкретная операция инкапсулирована в compareAndSet(). compareAndSet() сначала проверяет, равны ли текущая ссылка и текущий флаг ожидаемой ссылке и ожидаемому флагу, и, если оба равны, атомарно устанавливает значение ссылки и значение флага в заданное обновленное значение.

2.Длительное время цикла и высокие накладные расходы. Если операция CAS безуспешна в течение длительного времени, это приведет к ее постоянному вращению, что приведет к очень большим накладным расходам ЦП.

3.Только гарантированные атомарные операции над общей переменной. При выполнении операции с общей переменной CAS может гарантировать атомарность операции, но при работе с несколькими общими переменными CAS не может гарантировать атомарность операции.

Начиная с Java 1.5, JDK предоставляет класс AtomicReference для обеспечения атомарности между ссылочными объектами, и несколько переменных могут быть помещены в один объект для выполнения операций CAS.

2. Спин-блокировки против адаптивных спин-блокировок

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

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

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

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

图片3
Спиновая блокировка сама по себе несовершенна, она не заменяет обструкцию. В ожидании вращения, чтобы избежать накладных расходов на переключение потоков, но это занимает процессорное время. Если замок занят на короткое время, ожидание эффекта отжима будет очень хорошим. С другой стороны, если блокировка будет занята на долгое время, она будет крутить нити впустую, растрачивая ресурсы процессора. Итак, время ожидания спина должно иметь определенный предел, если спин превышает ограниченное количество раз (по умолчанию 10, можно использовать -XX:PreBlockSpin для изменения) без успеха для получения блокировки, то следует повесить поток.

Принцип реализации спин-блокировки также CAS. Цикл do-while в исходном коде, вызывающий unsafe для выполнения операции самоинкремента в AtomicInteger, является спин-операцией. Если модификация значения не удалась, спин выполняется через цикл до тех пор, пока модификация не будет успешной.

图片4
Спин-блокировки были представлены в JDK 1.4.2 и включены с помощью -XX:+UseSpinning. Он стал включен по умолчанию в JDK 6, и была введена адаптивная спин-блокировка (Adaptive Spinlock).

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

В спин-блокировках есть еще три распространенные формы блокировки: TicketLock, CLHlock и MCSlock. Эта статья только знакомит с терминами и не дает подробных объяснений. Заинтересованные студенты могут самостоятельно обратиться к соответствующей информации.

3. Lockless VS Biased Lock VS Легкий замок VS Тяжелый замок

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

Прежде всего, почему Synchronized может добиться синхронизации потоков?

Прежде чем ответить на этот вопрос, нам нужно понять два важных понятия: «Заголовок объекта Java», «Монитор».

Заголовок объекта Java

Synchronized — это пессимистическая блокировка. Прежде чем работать с ресурсом синхронизации, вам необходимо заблокировать ресурс синхронизации. Эта блокировка хранится в заголовке объекта Java, а что такое заголовок объекта Java?

Возьмем в качестве примера виртуальную машину Hotspot.Заголовок объекта Hotspot в основном включает в себя две части данных: Mark Word (отметить поле) и Klass Pointer (указатель типа).

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

Klass Point: указатель объекта на его метаданные класса, виртуальная машина использует этот указатель, чтобы определить, экземпляром какого класса является объект.

Monitor

Монитор можно понимать как инструмент синхронизации или механизм синхронизации, обычно описываемый как объект. Каждый объект Java имеет a, если не заблокирован, называемый внутренним замком или блокировкой монитора.

Монитор — это структура данных, приватная для потока.Каждый поток имеет список доступных записей монитора, а также глобальный доступный список. Каждый заблокированный объект связан с монитором, и в мониторе есть поле Owner, в котором хранится уникальный идентификатор потока, которому принадлежит блокировка, указывающий, что блокировка занята этим потоком.

Теперь вернемся к теме synchronized.Synchronized реализует синхронизацию потоков через Monitor, который полагается на Mutex Lock (блокировку взаимного исключения) базовой операционной системы для достижения синхронизации потоков.

Как мы упоминали в спин-блокировке, «блокировка или пробуждение потока Java требует, чтобы операционная система переключила состояние ЦП для завершения, этот переход состояния занимает время процессора. Если содержимое блока синхронизированного кода слишком простое, переход состояния потребляет время может быть больше, чем время выполнения пользовательского кода." Именно так синхронизированный был первоначально реализован для синхронизации, поэтому синхронизированный был неэффективен до JDK 6. Этот тип блокировки, который зависит от реализации Mutex Lock в операционной системе, называется «тяжеловесной блокировкой». представил.».

Таким образом, в настоящее время существует 4 состояния блокировки.Уровни от низкого к высокому: нет блокировки, предвзятая блокировка, легкая блокировка и тяжелая блокировка. Состояние блокировки можно только повысить, но не понизить.

Благодаря приведенному выше введению у нас есть понимание синхронизированного механизма блокировки и связанных с ним знаний, затем мы дадим содержание Mark Word, соответствующее четырем состояниям блокировки, а затем объясним идеи и характеристики четырех состояний блокировки:

статус блокировки хранить содержимое хранить содержимое
нет замка Хэш-код объекта, возраст генерации объекта, является ли это смещенной блокировкой (0) 01
Блокировка смещения Идентификатор предвзятого потока, предвзятая отметка времени, возраст создания объекта, является ли это предвзятой блокировкой (1) 01
Легкий замок указатель на запись блокировки в стеке 00
Блокировка тяжелой величины Указатель Mutex (тяжелый замок) 10

нет замка

Lock-free не блокирует ресурсы, все потоки могут получать доступ и изменять один и тот же ресурс, но только один поток может успешно изменять его одновременно.

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

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

Смещенная блокировка означает, что к фрагменту кода синхронизации всегда обращается поток, после чего поток автоматически получает блокировку, снижая затраты на получение блокировки.

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

Когда поток обращается к синхронизированному блоку и получает блокировку, идентификатор потока смещения блокировки сохраняется в слове маркировки. Когда поток входит в синхронизированный блок и выходит из него, он больше не блокируется и не разблокируется с помощью операций CAS, а определяет, сохранена ли в Mark Word смещенная блокировка, указывающая на текущий поток. Введение смещенных блокировок призвано свести к минимуму ненужные пути выполнения легковесных блокировок без многопоточной конкуренции, поскольку получение и освобождение легковесных блокировок зависят от нескольких атомарных инструкций CAS, а смещенным блокировкам нужно только заменить ThreadID. один раз полагаться на атомарную инструкцию CAS.

Смещенная блокировка снимает блокировку только тогда, когда другие потоки пытаются конкурировать за смещенную блокировку, и поток не будет активно снимать смещенную блокировку. Отзыв предвзятой блокировки должен дождаться глобальной безопасной точки (в этот момент времени байт-код не выполняется), он сначала приостановит поток, которому принадлежит предвзятая блокировка, чтобы определить, заблокирован ли объект блокировки. После отмены смещенной блокировки она восстанавливается в состояние отсутствия блокировки (бит флага «01») или облегченной блокировки (бит флага «00»).

Блокировка смещения включена по умолчанию в JDK 6 и более поздних версиях JVM. Смещенную блокировку можно отключить с помощью параметра JVM: -XX:-UseBiasedLocking=false, после закрытия программа по умолчанию войдет в состояние облегченной блокировки.

Легкий замок

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

Когда код входит в блок синхронизации, если состояние блокировки объекта синхронизации является состоянием отсутствия блокировки (флаг блокировки равен «01», независимо от того, является ли это смещенной блокировкой, равен «0»), виртуальная машина сначала установит кадр стека в текущем потоке Пространство с именем Lock Record используется для хранения копии текущего слова метки объекта блокировки, а затем копируется слово метки в заголовке объекта в запись блокировки.

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

Если операция обновления выполнена успешно, то поток владеет блокировкой объекта, а флаг блокировки объекта Mark Word устанавливается в «00», указывая на то, что объект находится в состоянии облегченной блокировки.

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

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

тяжелый замок

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

Общий процесс обновления состояния блокировки выглядит следующим образом:

图片5

Таким образом, предвзятая блокировка решает проблему блокировки путем сравнения с Mark Word и позволяет избежать выполнения операций CAS. Облегченные блокировки решают проблему блокировок, используя операции CAS и спины, чтобы избежать блокировки и пробуждения потоков и повлиять на производительность. Тяжеловесные блокировки блокируют все потоки, кроме потока, которому принадлежит блокировка.

4. Справедливая блокировка против несправедливой блокировки

Справедливая блокировка означает, что несколько потоков получают блокировки в том порядке, в котором они запрашивают блокировки.Потоки напрямую входят в очередь в очередь, и первый поток в очереди может получить блокировку. Преимущество справедливых блокировок заключается в том, что потоки, ожидающие блокировки, не будут голодать. Недостатком является то, что общая эффективность пропускной способности ниже, чем у несправедливых блокировок, все потоки, кроме первого потока в очереди ожидания, будут заблокированы, а накладные расходы ЦП на пробуждение заблокированных потоков выше, чем у несправедливых блокировок.

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

Это может быть немного абстрактно, чтобы описать непосредственно на языке.Здесь автор использует пример, увиденный в другом месте, чтобы говорить о честных и нечестных блокировках.

图片6
Как показано на рисунке выше, предположим, что есть колодец, который охраняет администратор.У администратора есть шлюз.Набирать воду может только тот, у кого есть замок.После откачки воды шлюз нужно вернуть на администратор. Каждый, кто приходит за водой, должен получить разрешение администратора и получить замок перед тем, как принести воду.Если кто-то приносит воду впереди, тот, кто хочет принести воду, должен стоять в очереди. Администратор проверит, является ли следующий человек за водой первым в очереди, если это так, то он закроет вас и позволит принести воду, если вы не первый человек в очереди, вы должны пойти в очередь Хвост очереди, это честная блокировка.

Но для несправедливых замков у администратора нет требований к тем, кто приносит воду. Даже если есть люди, ожидающие в очереди, если последний человек только что налил воду и вернул замок администратору, а администратор еще не разрешил следующему человеку в очереди принести воду, человек, который перерезает очередь приходит. , человек, который сократил очередь, может получить блокировку напрямую от администратора, чтобы принести воду без очереди, а люди, которые изначально ждали в очереди, могут только продолжать ждать. Как показано ниже:

图片7

Далее мы объясним честные и нечестные блокировки через исходный код ReentrantLock.

图片8
По коду есть внутренний класс SYNC, SYNC наследует AQS (AbstractQueuedSynchronizer), который собственно и реализован в SYNC. Он имеет справедливую блокировку FAIRSYNC и неполицейскую блокировку Nonfairsync двух подклассов. ReentrantLock по умолчанию использует блокировки без ошибок, или указанный конструктором использует справедливую блокировку.

Давайте посмотрим на исходный код метода блокировки справедливой блокировки и несправедливой блокировки:

图片9
Сравнивая исходный код на приведенном выше рисунке, мы ясно видим, что единственная разница между методом справедливой блокировки и недобросовестной блокировкой заключается в том, что справедливая блокировка имеет дополнительное ограничение при получении состояния синхронизации: hasQueuedPredecessors().
图片10
Снова введя hasQueuedPredecessors(), вы увидите, что этот метод в основном делает одну вещь: в основном определяет, является ли текущий поток первым в очереди синхронизации. Возвращает true, если да, иначе false.

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

5. Реентерабельные блокировки против нереентерабельных блокировок

Блокировка с повторным входом, также известная как рекурсивная блокировка, означает, что когда тот же поток получает блокировку во внешнем методе, внутренний метод, который входит в поток, автоматически получает блокировку (при условии, что объект блокировки должен быть тем же объектом или классом). он не будет заблокирован, потому что он был получен ранее и не был выпущен. Как ReentrantLock, так и synchronized в Java являются реентерабельными блокировками.Одним из преимуществ реентерабельных блокировок является то, что в определенной степени можно избежать взаимоблокировок. Для анализа используется следующий пример кода:

public class Widget {
    public synchronized void doSomething() {
        System.out.println("方法1执行...");
        doOthers();
    }

    public synchronized void doOthers() {
        System.out.println("方法2执行...");
    }
}

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

Если это блокировка без повторного входа, перед вызовом doOthers() текущий поток должен снять блокировку текущего объекта, полученную при выполнении doSomething(). Фактически, блокировка объекта уже удерживается текущим потоком и не может быть снята. выпущенный. Так что в это время будет тупик.

И почему реентерабельные блокировки могут автоматически получать блокировки при вложенных вызовах? Разберем его отдельно через схему и исходный код.

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

图片11
Но если это блокировка без повторного входа, администратор разрешает привязывать блокировку только к одному сегменту одного и того же человека. После того, как первое ведро привязано к замку, замок не будет разблокирован после набора воды, так что второе ведро не может быть привязано к замку и не может набирать воду. Текущий поток заблокирован, и все потоки во всей очереди ожидания не могут быть разбужены.
图片12
Ранее мы говорили, что ReentrantLock и synchronized являются реентерабельными блокировками, поэтому давайте сравним и проанализируем исходный код реентерабельной блокировки ReentrantLock и нереентерабельной блокировки NonReentrantLock, почему нереентерабельные блокировки вызывают тупиковые ситуации при повторном вызове ресурсов синхронизации.

Во-первых, и ReentrantLock, и NonReentrantLock наследуют родительский класс AQS. Родительский класс AQS поддерживает состояние синхронизации для подсчета количества повторных входов. Начальное значение состояния равно 0.

Когда поток пытается получить блокировку, повторно входящая блокировка сначала пытается получить и обновить значение состояния.Если состояние == 0 означает, что ни один другой поток не выполняет код синхронизации, статус устанавливается на 1, и текущий поток запускается выполнить. Если status != 0, определить, является ли текущий поток тем потоком, который получил блокировку, если да, выполнить status+1, и текущий поток снова сможет получить блокировку. Блокировка без повторного входа заключается в том, чтобы напрямую получить и попытаться обновить значение текущего состояния.Если состояние != 0, получить блокировку не удастся, и текущий поток будет заблокирован.

При освобождении блокировки повторно входящая блокировка также сначала получает значение текущего состояния, исходя из того, что текущий поток является потоком, удерживающим блокировку. Если status-1 == 0, это означает, что все повторные операции получения блокировки текущего потока были выполнены, и тогда поток фактически снимет блокировку. Блокировка без повторного входа заключается в том, чтобы напрямую установить статус в 0 и снять блокировку после определения того, что текущий поток является потоком, удерживающим блокировку.

图片13

6. Эксклюзивная блокировка против общей блокировки

Эксклюзивные блокировки и общие блокировки также являются концепцией. Давайте сначала представим конкретные концепции, а затем представим эксклюзивные блокировки и общие блокировки через исходный код ReentrantLock и ReentrantReadWriteLock.

Эксклюзивная блокировка, также называемая монопольной блокировкой, означает, что блокировка может одновременно удерживаться только одним потоком. Если поток T добавляет монопольную блокировку к данным A, другие потоки не могут добавлять какие-либо блокировки к A. Поток, получивший эксклюзивную блокировку, может как читать, так и изменять данные. Классом реализации synchronized в JDK и Lock в JUC является блокировка взаимного исключения.

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

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

На следующем рисунке показана часть исходного кода ReentrantReadWriteLock:

图片14
Мы видим, что ReentrantReadWriteLock имеет две блокировки: ReadLock и WriteLock, которые известны под словом «блокировка чтения» и «блокировка записи», вместе называемые «блокировками чтения-записи». Дальнейшее наблюдение показывает, что ReadLock и WriteLock — это блокировки, реализованные внутренним классом Sync. Синхронизация является подклассом AQS, и эта структура также существует в CountDownLatch, ReentrantLock и Semaphore.

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

В чем разница между конкретными методами блокировки блокировок чтения и блокировок записи? Прежде чем понять исходный код, нам нужно просмотреть другие знания. Когда мы впервые упомянули AQS, мы также упомянули поле состояния (тип int, 32 бита), которое используется для описания количества потоков, удерживающих блокировку.

В эксклюзивных блокировках это значение обычно равно 0 или 1 (если это блокировка с повторным входом, значением состояния является количество повторных входов), а в разделяемых блокировках состояние — это количество удерживаемых блокировок. Однако в ReentrantReadWriteLock есть две блокировки на чтение и запись, поэтому необходимо описать количество блокировок чтения и блокировок записи (или состояния) в целочисленной переменной state соответственно. Таким образом, переменная состояния «побитово разрезается» на две части: старшие 16 бит представляют статус блокировки чтения (количество блокировок чтения), а младшие 16 бит представляют статус блокировки записи (количество блокировок записи). Как показано ниже:

图片15

После понимания концепции давайте посмотрим на код, сначала посмотрим на исходный код блокировки записи:

protected final boolean tryAcquire(int acquires) {
	Thread current = Thread.currentThread();
	int c = getState(); // 取到当前锁的个数
	int w = exclusiveCount(c); // 取写锁的个数w
	if (c != 0) { // 如果已经有线程持有了锁(c!=0)
    // (Note: if c != 0 and w == 0 then shared count != 0)
		if (w == 0 || current != getExclusiveOwnerThread()) // 如果写线程数(w)为0(换言之存在读锁) 或者持有锁的线程不是当前线程就返回失败
			return false;
		if (w + exclusiveCount(acquires) > MAX_COUNT)    // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
      throw new Error("Maximum lock count exceeded");
		// Reentrant acquire
    setState(c + acquires);
    return true;
  }
  if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 如果当且写线程数为0,并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。
		return false;
	setExclusiveOwnerThread(current); // 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者
	return true;
}
  • Этот код сначала получает текущее количество блокировок c, а затем получает количество w блокировок записи до c. Поскольку блокировка записи — это младшие 16 бит, возьмите максимальное значение младших 16 бит и выполните операцию И с текущим c ( int w = exclusiveCount(c); ). Значение операции, а также количество потоки, удерживающие блокировку записи.
  • После получения количества потоков блокировки записи сначала определите, удерживают ли какие-либо потоки уже блокировку. Если поток уже удерживает блокировку (c!=0), проверьте количество текущих потоков блокировки записи.Если количество потоков записи равно 0 (т. е. в это время есть блокировка чтения) или поток, удерживающий блокировку не является текущим потоком, он вернет ошибку (включая реализацию справедливых и нечестных блокировок).
  • Ошибка выдается, если количество блокировок записи превышает максимальное число (65535, 2 в 16-й степени — 1).
  • Если количество потоков записи равно 0 (тогда поток чтения также должен быть равен 0, т. к. случай c!=0 был рассмотрен выше), а текущий поток необходимо заблокировать, то вернуть ошибку; писать потоки через CAS не удается, он также возвращает сбой.
  • Если c=0, w=0 или c>0, w>0 (реентерабельный), установите владельца текущего потока или заблокируйте его и верните успех!

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

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

Затем идет код для блокировки чтения:

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;                                   // 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

Видно, что в методе tryAcquireShared(int unused), если другие потоки получили блокировку записи, текущий поток не может получить блокировку чтения и переходит в состояние ожидания. Если текущий поток получает блокировку записи или блокировка записи не получена, текущий поток (потокобезопасность, полагаясь на гарантию CAS) увеличивает состояние чтения и успешно получает блокировку чтения. Каждый выпуск блокировки чтения (поточно-ориентированный, может быть несколько потоков чтения, одновременно освобождающих блокировку чтения) уменьшает состояние чтения на «1

На этом этапе давайте вернемся к исходному коду справедливой блокировки и недобросовестной блокировки в блокировке взаимного исключения ReentrantLock:

图片16
Мы обнаружили, что хотя в ReentrantLock есть справедливые и нечестные блокировки, все они добавляют эксклюзивные блокировки. Согласно исходному коду, когда поток вызывает метод блокировки для получения блокировки, если ресурс синхронизации не заблокирован другими потоками, текущий поток успешно вытеснит ресурс после использования CAS для успешного обновления состояния. Если публичный ресурс занят и не занят текущим потоком, то блокировка не удастся. Следовательно, можно определить, что добавленные блокировки ReentrantLock являются монопольными блокировками независимо от операций чтения или записи.

Эпилог

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

На самом деле в самой Java реализована хорошая инкапсуляция самой блокировки, что снижает сложность ее использования в повседневной работе студентов R&D. Тем не менее, студенты R&D также должны быть знакомы с основными принципами замков и выбирать наиболее подходящие замки в различных сценариях. Все идеи в исходном коде очень хорошие, и их также стоит изучить и на них можно ссылаться.

использованная литература

1. «Искусство параллельного программирования на Java» 2.Блокировки в Java 3.Анализ принципа Java CAS 4.Параллелизм Java — синхронизированный анализ ключевых слов 5.Краткое изложение принципа синхронизации Java 6.Разговор о параллелизме (2) - Синхронизировано в Java SE1.6 7.Глубокое понимание блокировок чтения-записи — анализ исходного кода ReadWriteLock 8.[JUC] ReentrantReadWriteLock для анализа исходного кода JDK1.8 9.Углубленный анализ многопоточности ReentrantReadWriteLock в Java (10) 10.Java -- принцип реализации блокировки чтения-записи

об авторе

Цзяци, технический инженер Meituan-Dianping. Он присоединился к Meituan-Dianping в 2017 году и отвечает за развитие бизнеса Meituan-Dianping, связанного с домашним отдыхом.