Ставьте лайк и смотрите снова, формируйте привычку, ищите в WeChat【Третий принц Ао Бин】Следуйте за этим дураком, у которого, кажется, что-то есть
эта статьяGitHub github.com/JavaFamilyОн был включен, и есть полные тестовые сайты, материалы, шаблоны резюме и моя программа жизни для интервью с производителями первой линии.
предисловие
В многопоточной разработке для управления синхронизацией потоков чаще всего используются ключевое слово synchronized и блокировка с повторным входом. В JDK8 было представлено новое оружие StampedLock. Что это? Английское слово Stamp означает штемпель. Так что же здесь имеется в виду? Для дураков см. разбивку ниже.
Перед лицом проблемы управления ресурсами критической области обычно существуют два набора идей:
Первый заключается в использовании пессимистической стратегии.Пессимисты думают, что: каждый раз, когда я обращаюсь к общим переменным в критической секции, кто-то всегда будет конфликтовать со мной.Поэтому я должен сначала блокировать весь объект каждый раз, когда я обращаюсь к нему, а затем разблокировать это после завершения доступа.
Наоборот, оптимисты считают, что, хотя общие переменные в критической секции будут конфликтовать, конфликт должен быть маловероятным событием. В большинстве случаев он не должен происходить. Поэтому я могу получить к нему доступ первым. После того, как данные не возник конфликт, моя операция выполнена успешно; если я обнаружу конфликт после ее использования, то я либо попытаюсь снова, либо переключусь на пессимистическую стратегию.
Отсюда нетрудно увидеть, что реентерабельные блокировки и синхронизация — типичная пессимистичная стратегия. Как вы, должно быть, догадались, StampedLock предоставляет инструмент для оптимистичных блокировок, поэтому он является важным дополнением к реентерабельным блокировкам.
Основное использование StampedLock
Очень хороший пример приведен в документации StampedLock, чтобы мы могли быстро понять использование StampedLock. Позвольте мне взглянуть на этот пример ниже, и пояснения к нему написаны в комментариях.
Вот еще одно объяснение значения метода validate().Сигнатура функции такая длинная:
public boolean validate(long stamp)
Его принятым параметром является почтовый штемпель, возвращенный последней операцией блокировки.Если блокировка не была применена для блокировки записи до вызова validate(), она вернет true, что также означает, что общие данные, защищенные блокировкой, не были изменены. , поэтому предыдущие операции чтения гарантированно обеспечивают целостность и согласованность данных.
И наоборот, если блокировка имеет успешное применение блокировки записи до проверки (), это означает, что предыдущие операции чтения и записи данных конфликтуют, и программе необходимо повторить попытку или обновиться до пессимистической блокировки.
Сравнение с повторными блокировками
Из вышеприведенного примера нетрудно увидеть, что с точки зрения сложности программирования StampedLock на самом деле гораздо сложнее входной блокировки, а код уже не такой лаконичный, как раньше.
Так почему мы вообще его используем?
Наиболее существенная причина заключается в повышении производительности! Вообще говоря, производительность этой оптимистической блокировки в несколько раз выше, чем у обычных блокировок с повторным входом, и по мере увеличения количества потоков разрыв в производительности будет становиться все больше и больше.
Короче говоря, производительность StampedLock подавляет повторные блокировки и блокировки чтения-записи в большом количестве одновременных сценариев.
Но ведь в мире нет ничего идеального, и StampedLock не всесилен, его недостатки заключаются в следующем:
- Кодирование доставляет больше хлопот: если используется оптимистическое чтение, конфликтующие сценарии должны обрабатываться сами по себе.
- Он не реентерабельный, и если вы случайно вызовете его дважды в одном и том же потоке, ваш мир будет чист. . . . .
- Он не поддерживает механизм ожидания/уведомления
Если вышеуказанные 3 пункта не являются для вас проблемой, то я считаю, что StampedLock должен быть вашим первым выбором.
внутренняя структура данных
Чтобы помочь вам лучше понять StampedLock, вот краткое введение в его внутреннюю реализацию и структуру данных.
В StampedLock есть очередь, в которой хранятся потоки, ожидающие блокировки. Очередь — это связанный список, а элемент в связанном списке — это объект с именем WNode:
Когда в очереди ожидают несколько потоков, вся очередь может выглядеть так:
В дополнение к этой очереди ожидания, еще одним особенно важным полем в StampedLock является длинное состояние, которое представляет собой 64-битное целое число, и StampedLock использует его очень умно.
Начальное значение состояния:
private static final int LG_READERS = 7;
private static final long WBIT = 1L << LG_READERS;
private static final long ORIGIN = WBIT << 1;
То есть ...0001 0000 0000 (спереди слишком много нулей, поэтому не пишите их, составьте 64~), почему бы вам не использовать здесь 0 в качестве начального значения? Поскольку 0 имеет особое значение, во избежание конфликтов было выбрано ненулевое число.
Если там занята блокировка записи, то пусть 7-й бит устанавливается в 1...0001 1000 0000, то есть добавляется WBIT.
Каждый раз, когда снимается блокировка записи, добавляется 1, но вместо непосредственного добавления состояния последний байт удаляется, и для статистики используются только первые 7 байтов. Поэтому после снятия блокировки записи состояние становится: ...0010 0000 0000, а после добавления еще одной блокировки становится: ...0010 1000 0000 и так далее.
Зачем нам нужно записывать, сколько раз была снята блокировка записи?
Это связано с тем, что решение штата по всему штату основано на операциях CAS. Обычные операции CAS могут столкнуться с проблемой ABA: если количество раз не записано, то при снятии, подаче заявки и снятии блокировки записи мы не сможем определить, были ли данные записаны. Здесь записывается количество релизов, поэтому, когда происходит «релиз-> приложение-> релиз», операция CAS может проверить изменения данных, чтобы определить, что операция записи произошла.Как оптимистическая блокировка, это может быть точным Считается, что конфликт произошел, а все остальное остается на усмотрение приложения для разрешения конфликта. Поэтому здесь записывается количество снятий блокировки для точного отслеживания конфликтов потоков.
7 бит оставшегося байта состояния используются для записи количества потоков, считывающих блокировку.Поскольку битов всего 7, можно записать только плохие 126. Посмотрите на RFULL в следующем коде, который является количество полностью загруженных потоков чтения. . Что делать, если превышает, избыточная часть записывается в поле readerOverflow.
private static final long WBIT = 1L << LG_READERS;
private static final long RBITS = WBIT - 1L;
private static final long RFULL = RBITS - 1L;
private transient int readerOverflow;
Подводя итог, структура переменной состояния выглядит следующим образом:
Написать приложение блокировки и освободить
Разобравшись с внутренней структурой данных StampedLock, давайте взглянем на приложение и снятие блокировок записи! Первый — подать заявку на блокировку записи:
public long writeLock() {
long s, next;
return ((((s = state) & ABITS) == 0L && //有没有读写锁被占用,如果没有,就设置上写锁标记
U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
//如果写锁占用成功范围next,如果失败就进入acquireWrite()进行锁的占用。
next : acquireWrite(false, 0L));
}
Если CAS не может установить состояние, это означает, что приложение блокировки записи не работает.В это время будет вызвана функцияAcquireWrite() для применения или ожидания. AcquireWrite() обычно выполняет следующие действия:
- присоединиться к команде
- Если головной узел равен хвостовому узлу
wtail == whead
, Это значит, что моя очередь, так что крутись и жди, и все кончено, когда ты схватишь его - если
wtail==null
, указывая, что очередь не инициализирована, просто инициализируйте очередь - Если в очереди есть другие ожидающие узлы, то можно только честно встать в очередь и ждать.
- Если головной узел равен хвостовому узлу
- заблокировать и ждать
- Если головной узел равен узлу-предшественнику
(h = whead) == p)
, значит уже почти моя очередь, я все крутлюсь и жду драки - В противном случае разбудите поток чтения в головном узле.
- Если блокировка не может быть вытеснена, то запарковать () текущий поток
- Если головной узел равен узлу-предшественнику
Проще говоря, функцияAcquireWrite() используется для борьбы за блокировку, а ее возвращаемое значение является почтовым штемпелем, представляющим текущий статус блокировки.В то же время для повышения производительности блокировки повторных попыток вращения, поэтому код выглядит немного неясным.
Снятие блокировки записи происходит следующим образом: входным параметром функции unlockWrite() является штемпель, полученный при подаче заявки на блокировку:
public void unlockWrite(long stamp) {
WNode h;
//检查锁的状态是否正常
if (state != stamp || (stamp & WBIT) == 0L)
throw new IllegalMonitorStateException();
// 设置state中标志位为0,同时也起到了增加释放锁次数的作用
state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
// 头结点不为空,尝试唤醒后续的线程
if ((h = whead) != null && h.status != 0)
//唤醒(unpark)后续的一个线程
release(h);
}
Прочтите приложение блокировки и отпустите
Код для получения блокировки чтения выглядит следующим образом:
public long readLock() {
long s = state, next;
//如果队列中没有写锁,并且读线程个数没有超过126,直接获得锁,并且读线程数量加1
return ((whead == wtail && (s & ABITS) < RFULL &&
U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
//如果争抢失败,进入acquireRead()争抢或者等待
next : acquireRead(false, 0L));
}
Реализация AcquiRead() довольно сложна и может быть грубо разделена на следующие шаги:
Короче говоря, это вращение, вращение и снова вращение, через непрерывное вращение, чтобы избежать действительной приостановки потока, насколько это возможно, только когда вращение терпит неудачу, нить действительно будет ждать.
Ниже описан процесс снятия блокировки чтения:
StampedLock пессимистично читает полную проблему с ЦП
StampedLock — хорошая штука, но из-за того, что она такая сложная, неизбежны небольшие проблемы. В следующем примере демонстрируется проблема пессимистической блокировки StampedLock, которая сводит ЦП с ума:
public class StampedLockTest {
public static void main(String[] args) throws InterruptedException {
final StampedLock lock = new StampedLock();
Thread t1 = new Thread(() -> {
// 获取写锁
lock.writeLock();
// 模拟程序阻塞等待其他资源
LockSupport.park();
});
t1.start();
// 保证t1获取写锁
Thread.sleep(100);
Thread t2 = new Thread(() -> {
// 阻塞在悲观读锁
lock.readLock();
});
t2.start();
// 保证t2阻塞在读锁
Thread.sleep(100);
// 中断线程t2,会导致线程t2所在CPU飙升
t2.interrupt();
t2.join();
}
}
В приведенном выше коде после прерывания t2 использование ЦП t2 будет заполнено на 100%. В это время t2 блокирует функцию readLock(), другими словами, после прерывания блокировка чтения StampedLock может занять ЦП. Что является причиной этого? Маленький дурак механизма, должно быть, подумал, что это вызвано слишком большим вращением StampedLock! Да, ваша догадка верна.
Конкретные причины следующие:
Если нет прерывания, поток, заблокированный в readLock(), перейдет в режим ожидания park() после нескольких вращений.После того как он войдет в режим ожидания park(), он не будет занимать ЦП. Но у функции park() есть особенность, что как только поток будет прерван, park() немедленно вернётся, возврат не засчитывается, и она не выдаст вам исключение, что смущает. Изначально вы хотели разблокировать() поток, когда блокировка была готова, но теперь, когда блокировка не годится, вы прямо прерывали, и парк() тоже возвращался, но, в конце концов, блокировка была плохой, поэтому я пошел чтобы себя закрутить.
Вращайтесь и поворачивайтесь, и переходите к функции park(), но, к сожалению, флаг прерывания потока всегда включен, а park() не может быть заблокирована, поэтому следующее вращение начинается снова, бесконечно. так что ЦП был заполнен.
Чтобы решить эту проблему, по сути, он должен быть внутри StampedLock.Когда park() возвращается, необходимо оценить метку прерывания и выполнить правильную обработку, такую как выход, создание исключения или очистка бита прерывания, все из которых может решить проблему.
Но к сожалению, по крайней мере в JDK8 такого лечения нет. Поэтому возникает описанная выше проблема, когда ЦП переполняется после прерывания readLock(). Пожалуйста, обратите внимание.
напиши в конце
Сегодня мы более подробно рассказали об использовании и основных идеях реализации StampedLock, который является важным дополнением к реентерабельным блокировкам и блокировкам чтения-записи.
Он обеспечивает оптимистическую стратегию блокировки и представляет собой другую реализацию блокировки. Конечно, с точки зрения сложности программирования StampedLock немного более громоздкий, чем входные блокировки и блокировки чтения-записи, но он увеличивает производительность.
Вот несколько небольших предложений для вас: если количество потоков приложения контролируемо, и их не так много, а конкуренция не слишком жесткая, то мы можем напрямую использовать простые синхронизированные, реентерабельные блокировки и блокировки чтения-записи; количество потоков приложений. Их много, жесткая конкуренция и требования к производительности, поэтому нам все еще нужно много работать и использовать более сложный StampedLock для повышения пропускной способности программы.
Есть два момента, на которые следует обратить особое внимание при использовании StampedLock: во-первых, StampedLock не является реентерабельным, поэтому не блокируйте себя в одном потоке. Во-вторых, StampedLock не имеет механизма ожидания/уведомления. можно только использовать его можно обойти его!
Есть ли у маленьких дураков более глубокое понимание этой непопулярной категории? Если понял, можешь поднять волну в комментариях: стань сильнее
Я Ао Бин, чем больше ты знаешь, тем больше ты не знаешь, увидимся в следующий раз.
Статья постоянно обновляется, вы можете искать в WeChat "Третий принц Ао Бин"Прочтите это в первый раз, ответьте [материал] Подготовленные мной материалы интервью и шаблоны резюме крупных заводов первой линии, эта статьяGitHub github.com/JavaFamilyОн был включен, и есть полные тестовые сайты для интервью с крупными заводами.Добро пожаловать в Star.