Каждый старший Java-программист, имевший опыт разработки многопоточных программ, должен задать себе вопрос, как реализована встроенная блокировка в Java? Наиболее часто используемая и самая простая блокировка — ReentrantLock. Если она не будет успешно добавлена немедленно, она заблокирует текущий поток и будет ждать, пока другие потоки снимут блокировку, прежде чем повторить попытку блокировки. Как поток блокирует сам себя? Что, если другие потоки разбудят текущий поток после снятия блокировки? Как текущий поток приходит к выводу, что он не успешно заблокировал блокировку? Эта статья ответит на все вопросы, упомянутые выше, от корня
примитив блокировки потока
Блокировка и пробуждение потока в Java выполняются с помощью методов парковки и разпарковки класса Unsafe.
public class Unsafe {
...
public native void park(boolean isAbsolute, long time);
public native void unpark(Thread t);
...
}
Оба эти метода являются собственными методами, которые сами по себе являются основными функциями, реализованными в языке C. park означает остановить и позволить текущему потоку Thread.currentThread() спать, а unpark означает отменить парковку и разбудить указанный поток. Эти два метода реализованы внизу с помощью механизма семафоров, предоставляемого операционной системой. Конкретный процесс реализации требует углубления в код C, и мы пока не будем его здесь анализировать. Два параметра метода park используются для управления продолжительностью сна.Первый параметр isAbsolute указывает, является ли второй параметр абсолютным или относительным временем в миллисекундах.
Поток будет продолжать работать с момента его запуска. За исключением политики планирования задач операционной системы, он будет приостановлен только при вызове парковки. Секрет того, что блокировка может приостановить поток, заключается именно в том, что блокировка вызывает метод парковки под капотом.
parkBlocker
В объекте потока Thread есть важный атрибут parkBlocker, который сохраняет то, для чего припаркован текущий поток. Это похоже на множество автомобилей, припаркованных на стоянке.Эти автовладельцы собрались здесь, чтобы участвовать в аукционе.Сфотографировав понравившиеся предметы, они уезжают. Тогда parkBlocker здесь, вероятно, ссылается на этот «аукцион». Это менеджер-координатор ряда конфликтующих потоков, и он контролирует, какой поток должен переходить в спящий режим, а какой просыпаться.
class Thread {
...
volatile Object parkBlocker;
...
}
Когда поток пробуждается unpark, этому свойству будет присвоено значение null. Unsafe.park и unpark не помогут нам установить свойство parkBlocker.Класс инструмента, отвечающий за управление этим свойством, — LockSupport, который просто обертывает два метода Unsafe.
class LockSupport {
...
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(false, 0L);
setBlocker(t, null); // 醒来后置null
}
public static void unpark(Thread thread) {
if (thread != null)
U.unpark(thread);
}
}
...
}
Структура данных блокировки Java реализована путем вызова LockSupport для перехода в спящий режим и пробуждения. Значение поля parkBlocker в объекте потока — это «менеджер очередей», о котором мы поговорим ниже.
администратор очередей
Когда несколько потоков соревнуются за одну и ту же блокировку, должен существовать механизм организации очередей, объединяющий те потоки, которым не удалось получить блокировку. Когда блокировка снимается, диспетчер блокировок выбирает соответствующий поток для удержания только что снятой блокировки. Такой менеджер очередей будет внутри каждого замка, и менеджер будет поддерживать очередь ожидающих потоков. Диспетчер очередей в ReentrantLock — это AbstractQueuedSynchronizer, а его внутренняя очередь ожидания представляет собой структуру двустороннего списка.Структура каждого узла в списке следующая.
class AbstractQueuedSynchronizer {
volatile Node head; // 队头线程将优先获得锁
volatile Node tail; // 抢锁失败的线程追加到队尾
volatile int state; // 锁计数
}
class Node {
Node prev;
Node next;
Thread thread; // 每个节点一个线程
// 下面这两个特殊字段可以先不去理解
Node nextWaiter; // 请求的是共享锁还是独占锁
int waitStatus; // 精细状态描述字
}
Если блокировка не удалась, текущий поток добавит себя в конец списка ожидания, а затем вызовет LockSupport.park, чтобы заснуть. Когда другие потоки разблокированы, они берут узел из головы связанного списка и вызывают LockSupport.unpark, чтобы разбудить его.
Класс AbstractQueuedSynchronizer — это абстрактный класс. Это родительский класс для всех администраторов очередей блокировок. Все формы блокировок в JDK и их внутренние администраторы очередей наследуют этот класс. Это краеугольный камень параллельного мира Java. Например, диспетчеры очередей внутри ReentrantLock, ReadWriteLock, CountDownLatch, Semaphone и ThreadPoolExecutor являются его подклассами. Этот абстрактный класс предоставляет некоторые абстрактные методы, и каждый тип блокировки требует настройки менеджера. И все параллельные структуры данных, построенные в JDK, завершаются под защитой этих замков, что является фундаментом многопоточной многоэтажки JDK.
То, что поддерживает диспетчер блокировок, — это обычная очередь в виде двустороннего списка.Эта структура данных очень проста, но довольно сложна в тщательном обслуживании, потому что требует тщательного рассмотрения проблем многопоточного параллелизма, и каждый строка кода написана с большой осторожностью.
Разработчиком менеджера блокировок JDK является Дуглас С. Леа. Он написал почти все параллельные пакеты Java самостоятельно. В мире алгоритмов чем сложнее вещи, тем больше подходит для одного человека.
Дуглас С. Леа — профессор компьютерных наук и в настоящее время заведующий кафедрой информатики в Университете штата Нью-Йорк в Освего, специализирующийся на параллельном программировании и проектировании параллельных структур данных. Он является членом исполнительного комитета Java Community Process и возглавляет JSR 166, который добавляет утилиты параллелизма к языку программирования Java.
Позже мы будем сокращать AbstractQueuedSynchronizer до AQS. Я должен напомнить читателям, что AQS слишком сложен, и вполне нормально столкнуться с неудачами в его понимании. В настоящее время на рынке нет книги, которая могла бы легко понять AQS, слишком мало людей, которые могут понять AQS, и я не считаю себя.
Справедливая блокировка и нечестная блокировка
Честные блокировки обеспечивают порядок, в котором запрашиваются и получаются блокировки.Если блокировка находится в свободном состоянии в определенный момент и поток пытается ее заблокировать, справедливая блокировка также должна проверять, есть ли в данный момент в очереди другие потоки, которые несправедливо. Блокировка может перейти прямо в очередь. Вспомните сцену в очереди, когда вы покупаете гамбургер в KFC.
Вы можете спросить, если блокировка свободна, как она может иметь потоки в очереди? Мы предполагаем, что поток, удерживающий блокировку, только что снял блокировку в этот момент, он будит первый поток node в очереди ожидания, а пробужденный поток только что вернулся из метода park, и затем он попытается заблокировать, затем из парка Возврат в состояние между блокировками — это свободное состояние блокировки, которое является кратковременным, и другие потоки также могут пытаться заблокироваться в течение этого короткого периода времени.
Во-вторых, следует отметить, что после того, как поток, выполняющий метод Lock.park, засыпает сам, ему не нужно ждать, пока другие потоки разпаркуют себя, прежде чем проснуться, он может проснуться в любой момент по какой-то неизвестной причине. Давайте посмотрим на комментарии к исходному коду, есть четыре причины, почему парк возвращается
- Другие потоки разблокировали текущий поток
- Время просыпаться естественным образом (у парка есть параметр времени)
- Другой поток прервал текущий поток
- Другие «ложные пробуждения» по неизвестным причинам
В документации четко не указано, какие неизвестные причины могут вызывать ложные пробуждения, но говорится, что когда метод парковки возвращается, это не означает, что блокировка свободна, и пробужденный поток снова припаркуется после того, как ему не удастся повторить попытку. получить замок. Следовательно, процесс блокировки должен быть записан в цикле, и может быть предпринято несколько попыток, прежде чем блокировка будет успешно получена.
Эффективность обслуживания несправедливых блокировок в компьютерном мире выше, чем у справедливых блокировок, поэтому блокировки Java по умолчанию используют несправедливые блокировки. Однако в реальном мире кажется, что эффективность несправедливых блокировок будет ниже, например, если вы можете продолжать сокращать очередь в KFC, вы можете представить, что сцена должна быть хаотичной. Причина, по которой существует разница между компьютерным миром и реальным миром, вероятно, заключается в том, что в компьютерном мире прыжок потока в очереди не вызывает жалоб других потоков.
public ReentrantLock() {
this.sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
this.sync = fair ? new FairSync() : new NonfairSync();
}
Общие блокировки и эксклюзивные блокировки
Блокировка ReentrantLock — это эксклюзивная блокировка, удерживаемая одним потоком, а все остальные потоки должны ждать. Блокировка чтения в ReadWriteLock не является монопольной блокировкой, она позволяет нескольким потокам одновременно удерживать блокировку чтения, которая является общей блокировкой. Общие и эксклюзивные блокировки различаются полем nextWaiter в классе Node.
class AQS {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
boolean isShared() {
return this.nextWaiter == SHARED;
}
}
Так почему же это поле не называется режимом или типом или просто называется общим? Это связано с тем, что в других сценариях метод nextWaiter используется по-разному.Это точно так же, как поле типа объединения языка C, но в языке Java нет типа объединения.
переменная условия
Что касается условных переменных, то первый вопрос, который необходимо задать, — зачем нужны условные переменные, а блокировок недостаточно? Рассмотрим следующий псевдокод, чтобы что-то сделать при выполнении определенного условия.
void doSomething() {
locker.lock();
while(!condition_is_true()) { // 先看能不能搞事
locker.unlock(); // 搞不了就歇会再看看能不能搞
sleep(1);
locker.lock(); // 搞事需要加锁,判断能不能搞事也需要加锁
}
justdoit(); // 搞事
locker.unlock();
}
Когда условие не выполняется, он повторяет попытку в цикле (другие потоки изменят условие путем блокировки), но требуются интервалы сна, иначе ЦП будет парить из-за простоя. Здесь есть проблема, то есть контролировать продолжительность сна непросто. Если интервал слишком длинный, это замедлит общую эффективность и даже упустит возможность (условия выполняются и сбрасываются немедленно), а если интервал слишком короткий, ЦП снова будет простаивать. Эту проблему можно решить с помощью условных переменных.
void doSomethingWithCondition() {
cond = locker.newCondition();
locker.lock();
while(!condition_is_true()) {
cond.await();
}
justdoit();
locker.unlock();
}
Метод await() всегда будет блокировать переменную условия cond до тех пор, пока другой поток не вызовет метод cond.signal() или cond.signalAll() перед возвратом. Когда await() блокируется, он автоматически снимает блокировку, удерживаемую текущим потоком. , после пробуждения await() он снова попытается удержать блокировку (может потребоваться снова поставить в очередь), и метод await() может успешно вернуться после успешного получения блокировки.
Может быть несколько потоков, заблокированных для условных переменных, и эти заблокированные потоки будут объединены в очередь ожидания условия. При вызове signalAll() все заблокированные потоки будут разбужены, и все заблокированные потоки снова начнут бороться за блокировку. Если вызывается функция signal(), она разбудит только поток в начале очереди, что позволяет избежать «проблемы шокирующего стада».
Метод await() должен снять блокировку немедленно, иначе другие потоки не смогут изменить состояние критической секции, а результат, возвращаемый функцией condition_is_true(), не изменится. Вот почему условная переменная должна быть создана объектом блокировки, а условная переменная должна содержать ссылку на объект блокировки, чтобы блокировку можно было снять и повторно заблокировать после активации сигналом. Блокировка, создающая условную переменную, должна быть эксклюзивной блокировкой. Если общая блокировка снимается методом await(), это не гарантирует, что состояние критической секции может быть изменено другими потоками. Только эксклюзивная блокировка может изменять состояние критической секции. Именно поэтому метод newCondition класса ReadWriteLock.ReadLock определяется следующим образом.
public Condition newCondition() {
throw new UnsupportedOperationException();
}
С условной переменной решается проблема плохого контроля сна. Когда условие выполнено, вызывается метод signal() или signalAll(), и заблокированный поток может быть немедленно разбужен практически без задержки.
очередь ожидания состояния
Когда несколько потоков await() используют одну и ту же условную переменную, формируется условная очередь ожидания. Одна и та же блокировка может создать несколько переменных условий, и будет несколько очередей ожидания условий. Эта очередь очень похожа на структуру очереди AQS, за исключением того, что это не двусторонняя, а односторонняя очередь. Узлы в очереди и узлы в очереди ожидания AQS относятся к одному и тому же классу, но указатели узлов не являются prev и next, а nextWaiter.
class AQS {
...
class ConditionObject {
Node firstWaiter; // 指向第一个节点
Node lastWaiter; // 指向第二个节点
}
class Node {
static final int CONDITION = -2;
static final int SIGNAL = -1;
Thread thread; // 当前等待的线程
Node nextWaiter; // 指向下一个条件等待节点
Node prev;
Node next;
int waitStatus; // waitStatus = CONDITION
}
...
}
передача очереди
При вызове метода signal() переменной условия будет пробужден поток головного узла очереди ожидания условия, узел будет удален из очереди ожидания условия, а затем переведен в очередь ожидания AQS, готовый к очереди попытаться повторно получить блокировку. В это время состояние узла меняется с CONDITION на SIGNAL, указывая на то, что текущий узел пробужден переменной условия и передан.
class AQS {
...
boolean transferForSignal(Node node) {
// 重置节点状态
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false
Node p = enq(node); // 进入 AQS 等待队列
int ws = p.waitStatus;
// 再修改状态为SIGNAL
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
...
}
Также изменилось значение поля nextWaiter передаваемого узла: в очереди условий это указатель следующего узла, в очереди ожидания AQS это признак разделяемой блокировки или мьютекса.
Процесс блокировки ReentrantLock
Далее мы подробно анализируем процесс блокировки и глубоко понимаем логику управления блокировкой. Я должен быть уверен, что код Dough Lea написан в минималистской форме, подобной следующей, которую довольно трудно читать.
class ReentrantLock {
...
public void lock() {
sync.acquire(1);
}
...
}
class Sync extends AQS {
...
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
...
}
Утверждение if для получения информации разделено на три части. Метод tryAcquire указывает, что текущий поток пытается заблокироваться. Если блокировка не удалась, его необходимо поставить в очередь. В это время вызывается метод addWaiter для присоединения к текущему потоку. . Затем вызовите методAcquireQueued, чтобы запустить циклический процесс блокировки с повторной попыткой парковки, пробуждения и повторной попытки блокировки, а также продолжения парковки, если блокировка не удалась. Метод получения не вернется, пока блокировка не будет успешной.
Метод AcquireQueued вернет значение true, если он будет прерван другим потоком во время циклического повтора блокировки. В это время поток должен вызвать метод selfInterrupt(), чтобы установить флаг прерывания для текущего потока.
// 打断当前线程,其实就是设置一个标识位
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
Как поток узнает, что он был прерван другим потоком? Вызовите Thread.interrupted() после того, как парк проснется, но этот метод можно вызвать только один раз, потому что он сбрасывает флаг прерывания сразу после вызова. Вот почему метод получения должен вызывать selfInterrupt(), чтобы сбросить флаг прерывания. Таким образом, логика верхнего уровня может знать, была ли она прервана с помощью Thread.interrupted().
МетодыAcquireQueued и addWaiter предоставляются классом AQS, а tryAcquire должен быть реализован самим подклассом. Разные замки имеют разные реализации. Давайте взглянем на реализацию метода справедливой блокировки tryAcquire ReentrantLock.
if(c == 0) означает, что текущая блокировка свободна и значение счетчика равно нулю. На этом этапе вам нужно бороться за блокировку, потому что может быть несколько потоков, вызывающих tryAcquire одновременно. Конкуренция заключается в использовании операции CAS compareAndSetState.Поток, который успешно изменит значение счетчика блокировок с 0 на 1, получит блокировку и запишет текущий поток в ExclusiveOwnerThread.
В коде также есть оценка hasQueuedPredecessors(). Эта оценка очень важна. Она означает, что есть ли другие потоки в текущей очереди ожидания AQS. Перед блокировкой необходимо проверить честную блокировку. Невозможно вскочить. Несправедливые блокировки не нуждаются в проверке, вся разница между честными блокировками и недобросовестными блокировками заключается в том, что эта проверка определяет, является ли блокировка честной или нет.
Давайте посмотрим на реализацию метода addWaiter.Режим параметра указывает, является ли это разделяемой блокировкой или монопольной блокировкой, что соответствует свойству Node.nextWaiter.
При добавлении нового узла в хвост очереди также необходимо учитывать многопоточный параллелизм, поэтому код снова использует операцию CAS compareAndSetTail, чтобы конкурировать за указатель хвоста. Потоки, которые не конкурировали, перейдут к следующему раунду конкуренции за (;;) Продолжайте использовать операцию CAS для добавления новых узлов в конец очереди.
Давайте взглянем на кодовую реализацию методаAcquireQueue.Он будет повторять цикл парковки, снова пытаться заблокировать и продолжать парковку, если блокировка не удалась.
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
После того, как поток просыпается после возврата из парка, он должен немедленно проверить, не прерывается ли он другими потоками. Однако, даже если произойдет прерывание, он продолжит попытки получить блокировку.Если это невозможно, он будет продолжать спать до тех пор, пока блокировка не будет получена, прежде чем вернуться в прерванное состояние. Это означает, что прерывание потока не приведет к его выходу в состоянии взаимоблокировки (неспособности получить блокировку).
В то же время мы также можем заметить, что блокировка может быть отменена cancelAcquire(), если быть точным, отмена находится в состоянии ожидания блокировки, а поток находится в очереди ожидания AQS в ожидании блокировки. При каких обстоятельствах будет выдано исключение и блокировка будет снята?Единственная возможность — это метод tryAcquire, который реализуется подклассом, поведение которого не контролируется AQS. Когда метод tryAcquire подкласса выдает исключение, лучший способ справиться с AQS — отменить блокировку. cancelAcquire удалит текущий узел из очереди ожидания.
Процесс разблокировки ReentrantLock
Процесс разблокировки проще: после уменьшения количества блокировок до нуля разбудите первый допустимый узел в очереди ожидания.
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 解铃还须系铃人
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
Что касается повторных блокировок, необходимо определить, падает ли счетчик блокировок до нуля, чтобы определить, снята ли блокировка полностью. Только когда блокировка полностью снята, можно пробудить последующие ожидающие узлы. unparkSuccessor пропустит недопустимые узлы (отмененные узлы), найдет первый действительный узел и вызовет unpark(), чтобы разбудить соответствующий поток.
Блокировка чтения-записи
Блокировки чтения-записи разделены на два объекта блокировки ReadLock и WriteLock, которые используют один и тот же AQS. Состояние переменной счетчика блокировок AQS будет разделено на две части: первые 16 бит — это счетчик ReadLock общей блокировки, а последние 16 бит — счетчик WriteLock блокировки мьютекса. Мьютекс записывает количество повторных входов в текущую блокировку записи, а общая блокировка записывает общее количество повторных входов всех потоков, удерживающих в данный момент общую блокировку чтения.
Блокировки чтения-записи также должны учитывать честные и нечестные блокировки. Стратегия справедливой блокировки общих блокировок и блокировок мьютексов такая же, как и у ReentrantLock, то есть, чтобы увидеть, есть ли другие потоки в настоящее время в очереди, и они будут послушно стоять в очереди до конца очереди. Стратегия недобросовестной блокировки отличается, она будет более склонна предоставлять больше возможностей для блокировок записи. Если в текущей очереди AQS есть какие-либо потоки запросов на чтение и запись, блокировка записи может быть запущена напрямую, но если заголовок очереди является запросом на блокировку записи, блокировка чтения должна дать возможность блокировке записи и очереди в конце очереди. В конце концов, блокировки чтения-записи подходят для случаев, когда операций чтения больше, а записей меньше, а случайный запрос на блокировку записи должен обрабатываться с более высоким приоритетом.
процесс записи блокировки
Общая логика блокировки записи блокировки чтения-записи такая же, как у ReentrantLock, отличие заключается в методе tryAcquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
Блокировки записи также необходимо считать повторно входящими.Если поток, удерживающий текущий мьютекс AQS, является именно тем потоком, который должен быть заблокирован, то блокировка записи является повторной, и для повторного входа необходимо только увеличить значение счетчика блокировок. Если c!=0, то есть счетчик блокировок не равен нулю, это может быть связано с тем, что текущий AQS имеет блокировку чтения или блокировку записи. замок.
Если значение счетчика равно нулю, начинается борьба за блокировку. В зависимости от того, является ли блокировка справедливой, вызовите метод WriterShouldBlock(), прежде чем состязаться, чтобы увидеть, нужно ли вам ставить в очередь. Если вам не нужно ставить в очередь, вы можете использовать операцию CAS для конкуренции. Поток, который успешно устанавливает значение счетчика из 0 на 1 будет писать исключительно Lock.
Чтение процесса блокировки блокировки
Процесс блокировки блокировок чтения намного сложнее, чем блокировок записи.Общий процесс такой же, как и блокировок записи, но детали очень разные. В частности, ему необходимо записывать количество блокировок чтения для каждого потока, а эта часть логики занимает много кода.
public final void acquireShared(int arg) {
// 如果尝试加锁不成功, 就去排队休眠,然后循环重试
if (tryAcquireShared(arg) < 0)
// 排队、循环重试
doAcquireShared(arg);
}
Если текущий поток уже удерживает блокировку записи, он может продолжить добавлять блокировку чтения, что является логикой, которая должна поддерживаться, чтобы добиться ухудшения блокировки. Понижение уровня блокировки означает добавление блокировки чтения, а затем снятие блокировки записи, пока удерживается блокировка записи. По сравнению с записью и разблокировкой, а затем чтением блокировки, это может сэкономить процесс блокировки и постановки в очередь во второй раз. Из-за деградации блокировки счетчики чтения и записи в счетчике блокировок могут быть ненулевыми одновременно.
wlock.lock();
if(whatever) {
// 降级
rlock.lock();
wlock.unlock();
doRead();
rlock.unlock();
} else {
// 不降级
doWrite()
wlock.unlock();
}
Чтобы подсчитать блокировки для каждого потока чтения, он устанавливает переменную ThreadLocal.
private transient ThreadLocalHoldCounter readHolds;
static final class HoldCounter {
int count;
final long tid = LockSupport.getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
Однако переменная ThreadLocal недостаточно эффективна для доступа, поэтому кеш устанавливается снова. Он хранит количество блокировок потока, который последним получил блокировку чтения. В случае, когда конфликты между потоками не особенно часты, более эффективно читать кэш напрямую.
private transient HoldCounter cachedHoldCounter;
Доу Леа счел, что использование cachedHoldCounter недостаточно эффективно, поэтому был добавлен еще один уровень кэш-записи firstReader для записи первого потока и счетчика блокировок, который изменил счетчик блокировок чтения с 0 на 1. Когда нет конфликта потоков, более эффективно читать эти два поля напрямую.
private transient Thread firstReader;
private transient int firstReaderHoldCount;
final int getReadHoldCount() {
// 先访问锁全局计数的读计数部分
if (getReadLockCount() == 0)
return 0;
// 再访问 firstReader
Thread current = Thread.currentThread();
if (firstReader == current)
return firstReaderHoldCount;
// 再访问最近的读线程锁计数
HoldCounter rh = cachedHoldCounter;
if (rh != null && rh.tid == LockSupport.getThreadId(current))
return rh.count;
// 无奈读 ThreadLocal 吧
int count = readHolds.get().count;
if (count == 0) readHolds.remove();
return count;
}
Итак, мы видим, что автор приложил большие усилия, чтобы записать этот счетчик блокировок чтения.Какова функция этого счетчика чтения? То есть поток может узнать, удерживает ли он блокировку чтения-записи через это значение счетчика.
Существует также процесс вращения в блокировке чтения.Так называемое вращение — это первый раз, когда блокировка терпит неудачу, поэтому она будет повторять попытку непосредственно в цикле без ожидания.Это немного похоже на метод повтора бесконечного цикла.
final static int SHARED_UNIT = 65536
// 读计数是高16位
final int fullTryAcquireShared(Thread current) {
for(;;) {
int c = getState();
// 如果有其它线程加了写锁,还是返回睡觉去吧
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
...
// 超出计数上限
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 拿到读锁了
...
return 1
}
...
// 循环重试
}
}
Поскольку блокировка чтения должна использовать операцию CAS для изменения общего значения счетчика чтения базовой блокировки, блокировка чтения может быть получена только в случае успешной блокировки чтения. между блокировками чтения идет конкуренция за операцию CAS, это не значит, что в данный момент блокировка занята другими и не может быть получена самостоятельно. После нескольких попыток блокировка пройдет успешно, что и является причиной спина. Существует также процесс циклического повтора операции CAS при снятии блокировки чтения.
protected final boolean tryReleaseShared(int unused) {
...
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) {
return nextc == 0;
}
}
...
}
резюме
Краеугольными камнями возможностей параллелизма в Java являются методы park() и unpark(), изменчивые переменные, синхронизация, операции CAS и очереди AQS.Углубиться в эти точки знаний непросто, и точки знаний, связанные с блокировкой, упомянутые в этом разделе Он не очень полный, и есть много деталей, которые я сам до конца не понял, поэтому мы поговорим о более подробном описании замка позже.