В предыдущей статье «Вы действительно понимаете ReentrantReadWriteLock? ", я оставляю вам введение, более эффективную блокировку чтения-записи, которая может избежать голодания при записи --- StampedLock. StampedLock понимает, что не только множественные чтения не блокируют друг друга, но и операции записи не блокируются во время операций чтения.
Почему StampedLock такой замечательный? Для достижения этого эффекта его основная идея заключается в том, что если запись происходит во время чтения, новое значение должно быть получено повторной попыткой, а операция записи не должна блокироваться. Этот режим также является типичной идеей программирования без блокировки, такой же, как и идея спина CAS. Этот метод работы определяет, что StampedLock очень подходит для сценариев, в которых имеется много потоков чтения и очень мало потоков записи, и в то же время он позволяет избежать зависания при записи. В этой статье StampedLock будет проанализирован по следующим пунктам.
- Анализ официального примера использования StampedLock
- Анализ исходного кода: количество состояний, разделяемое блокировками чтения-записи
- Анализ исходного кода: снятие и получение блокировок записи
- Анализ исходного кода: снятие и получение пессимистичных блокировок чтения
- Тестирование производительности
Анализ официального примера использования StampedLock
Давайте сначала посмотрим на официальный вариант использования StampedLock:
public class Point {
private double x, y;
private final StampedLock stampedLock = new StampedLock();
//写锁的使用
void move(double deltaX, double deltaY){
long stamp = stampedLock.writeLock(); //获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp); //释放写锁
}
}
//乐观读锁的使用
double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); //获得一个乐观读锁
double currentX = x;
double currentY = y;
if (!stampedLock.validate(stamp)) { //检查乐观读锁后是否有其他写锁发生,有则返回false
stamp = stampedLock.readLock(); //获取一个悲观读锁
try {
currentX = x;
} finally {
stampedLock.unlockRead(stamp); //释放悲观读锁
}
}
return Math.sqrt(currentX*currentX + currentY*currentY);
}
//悲观读锁以及读锁升级写锁的使用
void moveIfAtOrigin(double newX,double newY) {
long stamp = stampedLock.readLock(); //悲观读锁
try {
while (x == 0.0 && y == 0.0) {
long ws = stampedLock.tryConvertToWriteLock(stamp); //读锁转换为写锁
if (ws != 0L) { //转换成功
stamp = ws; //票据更新
x = newX;
y = newY;
break;
} else {
stampedLock.unlockRead(stamp); //转换失败释放读锁
stamp = stampedLock.writeLock(); //强制获取写锁
}
}
} finally {
stampedLock.unlock(stamp); //释放所有锁
}
}
}
Сначала посмотрите на первый метод move, вы можете увидеть, что это в основном то же самое, что и использование блокировки записи ReentrantReadWriteLock.Это простое получение и освобождение.Можно догадаться, что это также реализация монопольной блокировки. Следует отметить, что штамп типа long будет возвращен при получении блокировки записи, а затем штамп будет передан при освобождении блокировки записи. Для чего этот штамп? Что произойдет, если мы изменим это значение в середине? Я не буду пока объяснять это здесь, и я отвечу на этот вопрос позже при анализе исходного кода.
Второй метод DistanceFromOrigin совершенно особенный, он вызывает tryOptimisticRead, который является оптимистичной блокировкой чтения, основанной на имени. Прежде всего, что такое оптимистическая блокировка? Смысл оптимистической блокировки состоит в том, чтобы предположить, что общая переменная не будет изменена во время приобретения оптимистической блокировки, а поскольку предполагается, что она не будет изменена, ее не нужно блокировать. После получения оптимистической блокировки чтения выполняются некоторые операции, а затем вызывается метод проверки. Этот метод используется для проверки того, была ли операция записи выполнена после tryOptimisticRead. Если это так, блокировка чтения получена. Блокировка чтения здесь в ReentrantReadWriteLock Блокировка чтения аналогична, и предполагается, что это тоже разделяемая блокировка.
Третий метод, moveIfAtOrigin, выполняет операцию расширения блокировки. Вызывая tryConvertToWriteLock, он пытается преобразовать блокировку чтения в блокировку записи. После успешного преобразования это эквивалентно получению блокировки записи. эквивалентно занятой блокировке записи Вызовите writeLock, чтобы получить блокировку записи для операции.
Предполагается, что после прочтения трех вышеуказанных методов у всех сложилось предварительное представление о том, как использовать StampedLock. Ниже приведен пошаговый анализ исходного кода StampedLock, чтобы понять, как он решает проблему нехватки блокировки.
Анализ исходного кода: количество состояний, разделяемое блокировками чтения-записи
Из приведенного выше примера использования мы видим, что StampedLock, помимо предоставления метода получения и снятия блокировки чтения-записи, аналогичного ReentrantReadWriteLock, также предоставляет метод получения оптимистичной блокировки чтения. Так как же взаимодействуют эти три способа? Согласно опыту AQS, StampedLock также должен использовать количество состояний для обозначения состояния блокировки. Это можно продемонстрировать с помощью следующего исходного кода:
// 用于操作state后获取stamp的值
private static final int LG_READERS = 7;
private static final long RUNIT = 1L; //0000 0000 0001
private static final long WBIT = 1L << LG_READERS; //0000 1000 0000
private static final long RBITS = WBIT - 1L; //0000 0111 1111
private static final long RFULL = RBITS - 1L; //0000 0111 1110
private static final long ABITS = RBITS | WBIT; //0000 1111 1111
private static final long SBITS = ~RBITS; //1111 1000 0000
//初始化时state的值
private static final long ORIGIN = WBIT << 1; //0001 0000 0000
//锁共享变量state
private transient volatile long state;
//读锁溢出时用来存储多出的毒素哦
private transient int readerOverflow;
В приведенном выше исходном коде, в дополнение к определению переменной состояния, также предоставляется ряд переменных для управления состоянием, которые используются для представления различных состояний блокировок чтения и блокировок записи. Для облегчения понимания я буду представлять их в виде двоичных значений с ограниченной длиной, здесь младшие 12 бит используются для представления длинных 64, а старшие биты автоматически заполняются 0. Чтобы понять роль этих состояний, необходимо проанализировать, как три метода операции блокировки представлены переменным состоянием.Во-первых, давайте взглянем на получение и освобождение блокировок записи.
Анализ исходного кода: снятие и получение блокировок записи
public StampedLock() {
state = ORIGIN; //初始化state为 0001 0000 0000
}
public long writeLock() {
long s, next;
return ((((s = state) & ABITS) == 0L && //没有读写锁
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ? //cas操作尝试获取写锁
next : acquireWrite(false, 0L)); //获取成功后返回next,失败则进行后续处理,排队也在后续处理中
}
public void unlockWrite(long stamp) {
WNode h;
if (state != stamp || (stamp & WBIT) == 0L) //stamp值被修改,或者写锁已经被释放,抛出错误
throw new IllegalMonitorStateException();
state = (stamp += WBIT) == 0L ? ORIGIN : stamp; //加0000 1000 0000来记录写锁的变化,同时改变写锁状态
if ((h = whead) != null && h.status != 0)
release(h);
}
Вот два вывода: блокировка чтения представлена первыми 7 битами, и каждый раз, когда устанавливается блокировка чтения, добавляется 1. Блокировка записи представлена оставшимися битами после деления первых 7 битов.Каждый раз, когда устанавливается блокировка записи, добавляется 1000 0000. Обе эти точки можно доказать в следующем исходном коде.
Установите переменную состояния на 0001 0000 0000 при инициализации. Блокировка записи достигается через((s = state) & ABITS)
Когда операция равна 0, блокировки чтения и записи по умолчанию отсутствуют. Возможны три случая получения блокировки записи:
-
Когда нет блокировки чтения и записи, состояние равно 0001 0000 0000. 0001 0000 0000 и 0000 1111 1111 = 0000 0000 0000 // равно 0L, вы можете попытаться получить блокировку записи
-
Когда есть блокировка чтения, состояние 0001 0000 0001 0001 0000 0001 и 0000 1111 1111 = 0000 0000 0001 // не равно 0L
-
Есть блокировка записи с состоянием 0001 1000 0000 0001 1000 0000 и 0000 1111 1111 = 0000 1000 0000 // не равно 0L
Чтобы получить блокировку записи, вам нужно установить s + WBIT в состояние, то есть каждый раз, когда вы получаете блокировку записи, вам нужно добавить 0000 1000 0000. Также возвращает значение s + WBIT. 0001 0000 0000 + 0000 1000 0000 = 0001 1000 0000
Чтобы снять блокировку записи, сначала определите, было ли значение штампа изменено или снято несколько раз, а затем передайтеstate = (stamp += WBIT) == 0L ? ORIGIN : stamp
Чтобы снять блокировку записи, выполните следующие действия с битами:stamp += WBIT
0010 0000 0000 = 0001 1000 0000 + 0000 1000 0000
Этот шаг является ключевым! ! ! Освобождение блокировки записи не +1, а затем -1, как ReentrantReadWriteLock, а путем повторного добавления 0000 1000 0000, чтобы старший бит менялся каждый раз, зачем это делать? Вы не можете просто вычесть 0000 1000 0000 напрямую? Это должно проложить путь для оптимистичной блокировки в будущем, чтобы каждый раз, когда блокировка записывалась, оставались следы.
Вы можете представить себе такую сцену: буква А меняется на Б, и вы можете видеть это изменение. изменение не может быть записано Это проблема ABA в CAS. В StampedLock процесс записи операции блокировки записи достигается путем добавления 0000 1000 0000 к старшим битам каждый раз, что можно понять, выполнив следующие шаги:
- Получите блокировку записи в первый раз: 0001 0000 0000 + 0000 1000 0000 = 0001 1000 0000
- Снимите блокировку записи в первый раз: 0001 1000 0000 + 0000 1000 0000 = 0010 0000 0000
- Второй раз, чтобы получить блокировку записи: 0010 0000 0000 + 0000 1000 0000 = 0010 1000 0000
- Снимите блокировку записи во второй раз: 0010 1000 0000 + 0000 1000 0000 = 0011 0000 0000
- Получите блокировку записи в n-й раз: 1110 0000 0000 + 0000 1000 0000 = 1110 1000 0000
- Снимите блокировку записи в n-й раз: 1110 1000 0000 + 0000 1000 0000 = 1111 0000 0000
Можно видеть, что 8-й бит будет изменяться при получении и снятии блокировки записи, то есть 8-й бит используется для индикации состояния блокировки записи, первые 7 бит используются для индикации состояния блокировки чтения, а 8-й бит используется для индикации состояния блокировки чтения Указывает, сколько раз была получена блокировка записи. Это эффективно решает проблему ABA, оставляет запись о каждой блокировке записи и обеспечивает основу для последующих оптимистичных изменений проверки блокировки.
оacquireWrite
Подробно метод здесь не анализируется, метод очень сложен, заинтересованные студенты могут искать соответствующую информацию в Интернете. Вот лишь краткое изложение метода.Этот метод выполняет организацию очереди потоков в два этапа.Во-первых, несколько циклов используются для получения блокировки путем случайного обнаружения, а затем узел инициализируется для вставки после определенного количества неудачных циклов.
Анализ исходного кода: снятие и получение пессимистичных блокировок чтения
public long readLock() {
long s = state, next;
return ((whead == wtail && (s & ABITS) < RFULL && //队列为空,无写锁,同时读锁未溢出,尝试获取读锁
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ? //cas尝试获取读锁+1
next : acquireRead(false, 0L)); //获取读锁成功,返回s + RUNIT,失败进入后续处理,类似acquireWrite
}
public void unlockRead(long stamp) {
long s, m; WNode h;
for (;;) {
if (((s = state) & SBITS) != (stamp & SBITS) ||
(stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
throw new IllegalMonitorStateException();
if (m < RFULL) { //小于最大记录值(最大记录值127超过后放在readerOverflow变量中)
if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) { //cas尝试释放读锁-1
if (m == RUNIT && (h = whead) != null && h.status != 0)
release(h);
break;
}
}
else if (tryDecReaderOverflow(s) != 0L) //readerOverflow - 1
break;
}
}
Приобретение пессимистической блокировки чтения аналогично ReentrantReadWriteLock, за исключением того, что блокировку чтения StampedLock легко переполнить, максимальное значение всего 127, и после ее превышения она сохраняется через дополнительную переменную readerOverflow, которая должна оставить больше место для блокировки записи, потому что блокировка записи постоянно увеличивается. Пессимистическое получение блокировки чтения делится на следующие четыре случая:
-
Когда нет блокировки чтения и записи, состояние равно 0001 0000 0000. // 小于 0000 0111 1110,可以尝试获取读锁 0001 0000 0000 & 0000 1111 1111 = 0000 0000 0000
-
Когда есть блокировка чтения, состояние 0001 0000 0001 // Меньше 0000 0111 1110 можно попробовать получить ридлок 0001 000 0001 и 0000 1111 1111 = 0000 000 0001
-
Есть блокировка записи с состоянием 0001 1000 0000 // больше 0000 0111 1110, блокировка чтения не может быть получена 0001 1000 0000 и 0000 1111 1111 = 0000 1000 0000
-
Переполнение блокировки чтения, состояние 0001 0111 1110 // равно 0000 0111 1110, не может получить блокировку чтения 0001 0111 1110 и 0000 1111 1111 = 0000 0111 1110 Процесс снятия блокировки чтения выполняется без переполнения
s - RUNIT
Операция освобождается при -1, а переменная readerOverflow будет равна -1 при переполнении.
Получение и проверка оптимистичной блокировки чтения
Поскольку блокировка оптимистического чтения на самом деле не получила блокировку, нет процесса снятия блокировки, она только проходит проверку после операции и изменения перед получением. Исходный код выглядит следующим образом:
//尝试获取乐观锁
public long tryOptimisticRead() {
long s;
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
//验证乐观锁获取之后是否有过写操作
public boolean validate(long stamp) {
//该方法之前的所有load操作在内存屏障之前完成,对应的还有storeFence()及fullFence()
U.loadFence();
return (stamp & SBITS) == (state & SBITS); //比较是否有过写操作
}
Основной принцип оптимистической блокировки состоит в том, чтобы записать состояние записи состояния при получении блокировки, а затем проверить, изменилось ли состояние записи после завершения операции, потому что состояние записи будет оставлять запись в старшей позиции каждый раз. , чтобы избежать блокировки записи после его получения и снятия Точные данные недоступны. Существует три случая получения записей блокировки записи:
-
Когда нет блокировки чтения и записи, состояние равно 0001 0000 0000. //((s = состояние) & WBIT) == 0L) true 0001 0000 0000 и 0000 1000 0000 = 0000 0000 0000 //(s и SBITS) 0001 0000 0000 и 1111 1000 0000 = 0001 0000 0000
-
Когда есть блокировка чтения, состояние 0001 0000 0001 //((s = состояние) & WBIT) == 0L) true 0001 0000 0001 и 0000 1000 0000 = 0000 0000 0000 //(s и SBITS) 0001 0000 0001 и 1111 1000 0000 = 0001 0000 0000
-
Есть блокировка записи с состоянием 0001 1000 0000 //((s = состояние) & WBIT) == 0L) false 0001 1000 0000 и 0000 1000 0000 = 0000 1000 0000 //0 л 0000 0000 0000
Есть ли операция записи в процессе проверки, есть четыре случая
-
написал однажды 0001 0000 0000 и 1111 1000 0000 = 0001 0000 0000 0010 0000 0000 и 1111 1000 0000 = 0010 0000 0000//ложь
-
не написано, но читать 0001 0000 0000 и 1111 1000 0000 = 0001 0000 0000 0001 0000 1111 и 1111 1000 0000 = 0001 0000 0000 // true
-
正在写 0001 0000 0000 и 1111 1000 0000 = 0001 0000 0000 0001 1000 0000 & 1111 1000 0000 = 0001 1000 0000 //false
-
Перед записью не будет 0L закончена она или нет 0000 0000 0000 и 1111 1000 0000 = 0000 0000 0000//ложь
Тестирование производительности
После анализа принципа реализации StampedLock, вот тесты производительности StampedLock, ReentrantReadWriteLock и Synchronized в различных сценариях.Код теста принимает https://blog.takipi.com/java-8-stampedlocks-vs-readwritelocks- и -synchronized/ Для кода в статье сначала опубликуйте результаты теста в блоге выше. Режим ОПТИМИСТИК в статье принимает режим "грязного чтения". Результаты теста ОПТИМИСТИК здесь не используются, а только StampedLock, ReentrantReadWriteLock и Синхронизированные сравниваются.
5 потоков чтения и 5 потоков записи. Сценарий: лучше всего работает обычный режим StampedLock и ReentrantReadWriteLock.
10 потоков чтения и 10 потоков записи. Сценарий: лучше всего работает StampedLock в обычном режиме и режиме синхронизации.16 Читайте темы и 4 Сценарий напитков.19 цепочек чтения и 1 цепочка записи. Сценарий: Лучше всего работает синхронизация.В комментариях к блогу также есть тестовый сценарий с потоками чтения 2000 и потоком письма 1. Результаты теста следующие: StampedLock...12814.2 ReentrantReadWriteLock...18882.8 Synchronized...22696.4 Лучшим исполнителем является StampedLock.Прочитав приведенный выше тест, наилучшая производительность первых трех сценариев у StampedLock, но в четвертом случае StampedLock работает плохо, поэтому я протестировал код еще раз, и ввиду большого количества применений блокировок чтения-записи в кеше В сценарии огромный разрыв между чтением и письмом, я добавил 100 сценариев чтения и 1 сценарий письма.
Тестовая машина: ОС MAC (10.12.6), ЦП: Intel Core i5 с тактовой частотой 2,4 ГГц, память: 8 ГБ, версия ПО: JDK1.8 Результаты теста следующие: 19 потоков чтения и 1 сценарий потока записи: StampedLock и Synchronized — лучшие исполнители. Прочитать нить: 19. Написать нить: 1. Количество петель: 5. Рассчитать сумму: 1000000
Сценарий со 100 потоками чтения и 1 потоком записи: StampedLock и Synchronized — лучшие исполнители. Читать темы: 100. Записывать темы: 1. Количество петель: 5. Подсчитать сумму: 100000С помощью приведенных выше тестов можно обнаружить, что общая производительность StampedLock и Synchronized в среднем не сильно отличается, и StampedLock имеет небольшое преимущество при увеличении разрыва чтения-записи. Разница в производительности ReentrantReadWriteLock немного неожиданна, и она может в принципе доходить до отказа.Интересно, есть ли у вас какие-либо предложения по сценариям использования ReentrantReadWriteLock?В то же время, ввиду того, что исходное синхронизированное имеет большое пространство для оптимизации на более позднем этапе, и имеет определенные преимущества в сложности кода и безопасности, синхронизированные могут быть использованы для синхронизации в большинстве сценариев, а определенные требования к производительности Требуется в некоторых конкретных сценариях. ParmedLock можно использовать ниже. Код, использованный в тесте, можно найти в блогах, которые я ссылался. Вы можете попробовать тест самостоятельно. Если у вас есть какие-либо вопросы по поводу результатов, вы можете спросить их в комментариях.
Использованная литература: