Параллельные высокочастотные вопросы на собеседовании по Java: расскажите о своем понимании AQS?

интервью Java
Параллельные высокочастотные вопросы на собеседовании по Java: расскажите о своем понимании AQS?

Глубокое понимание AbstractQueuedSynchronizer

Есть чувства, есть галантерейные товары, поиск в WeChat【Третий принц Ао Бин] Подпишитесь на этого программиста, у которого есть кое-что.

эта статьяGitHub github.com/JavaFamilyВключено, и есть полные тестовые площадки, материалы и мой цикл статей для интервью с производителями первой линии.

В многопоточном программировании на Java реентерабельная блокировка (ReentrantLock) и семафор (Semaphore) являются двумя чрезвычайно важными инструментами управления параллелизмом. Я считаю, что большинство читателей должны быть знакомы с их использованием (если вы не уверены, достаньте книгу и быстро прочитайте ее).

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

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

Как видите, и реентерабельная блокировка, и семафор находятся внутри себя, и реализован подкласс AbstractQueuedSynchronizer, имя подкласса — Sync. И этот класс Sync также является основной реализацией реентерабельных блокировок и семафоров. Код в подклассе Sync также относительно невелик, а его основные алгоритмы предоставляются AbstractQueuedSynchronizer. Следовательно, можно сказать, что пока вы понимаете AbstractQueuedSynchronizer, вы будете знать принципы реализации реентерабельных блокировок и семафоров.

Узнайте об AbstractQueuedSynchronizer, о котором вы должны знать

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

Управление многопоточностью на основе разрешений

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

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

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

Эксклюзивные блокировки и общие блокировки

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

Говоря словами колеса обозрения, эксклюзивная блокировка заключается в том, что, хотя у меня здесь 20 мест, дети могут получить доступ к ним только по одному. А что насчет дополнительных мест? Вы можете оставить их пустыми или позволить единственному ребенку на колесе обозрения изменить их.Неважно, если он сидит, где он хочет, и меняет свое положение в течение 1 минуты. Общий замок — это обычный способ открыть колесо обозрения.

LockSupport

LockSupport можно понимать как класс инструмента. Его роль очень проста — приостановить и продолжить выполнение потока. Его обычно используемый API выглядит следующим образом:

  • public static void park() : приостанавливает текущий поток, если разрешение недоступно
  • public static void unpark (поток потока): дайте потоку доступное разрешение на продолжение выполнения.

Поскольку слово park означает остановку, функция park() здесь означает приостановку потока. И наоборот, unpark() означает, что поток продолжает выполняться.

Следует отметить, что сам LockSupport также является лицензионной реализацией, как понять это предложение, см. следующий код:

LockSupport.unpark(Thread.currentThread());
LockSupport.park();

Вы можете догадаться, что после выполнения функции park() текущий поток останавливается или может продолжать выполняться?

Ответ: можно продолжать. Это связано с тем, что функция unpark() выполняется до функции park(), которая освобождает лицензию, а это означает, что текущий поток имеет доступную лицензию. И park() не будет блокировать поток, если есть доступная лицензия.

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

По сравнению с park() и unpark() типичным отрицательным примером являются Thread.resume() и Thread.suspend().

См. код ниже:

Thread.currentThread().resume();
Thread.currentThread().suspend();

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

Поэтому, используя Park () и Unpark () - наш первый выбор. В Abstrueuedsynchronizer именно парк () и нечеткие () операции locksupport для управления запущенным состоянием нити.

Внутренняя структура данных AbstractQueuedSynchronizer

Итак, основная часть представлена ​​здесь. Теперь давайте перейдем к делу: сначала посмотрим на внутреннюю структуру данных AbstractQueuedSynchronizer.

Внутри AbstractQueuedSynchronizer есть очередь, мы называем еесинхронная очередь ожидания. Его роль заключается в сохранении потоков, ожидающих этой блокировки (из-за ожидания, вызванного операцией lock()). Кроме того, чтобы поддерживать ожидающий поток, ожидающий переменной условия, AbstractQueuedSynchronizer должен поддерживать еще одинОчередь ожидания переменной условия, то есть те потоки, которые заблокированы Condition.await().

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

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

Видно, что независимо от того, является ли это синхронной очередью ожидания или очередью ожидания с условной переменной, в качестве узла связанного списка используется один и тот же класс Node. Для синхронной очереди ожидания Node включает предыдущий элемент prev связанного списка, следующий элемент next и поток объекта thread. Для очереди ожидания переменных условия nextWaiter также используется для представления следующего узла, ожидающего в очереди переменных условий.

Другой важный член узла Node — это waitStatus, который указывает состояние узла, ожидающего в очереди:

  • ОТМЕНЕНО: Указывает, что поток отменил ожидание. Если в процессе получения блокировки возникают какие-либо исключения, может произойти отмена, например исключение прерывания или тайм-аут во время процесса ожидания.
  • СИГНАЛ: Указывает, что последующие узлы необходимо разбудить.
  • Условие: ожидание потока в очереди переменных условия.
  • РАСПРОСТРАНЕНИЕ: в общем режиме безоговорочно распространять состояние releaseShared. В раннем JDK такого состояния не было, на первый взгляд, это состояние избыточно. Это состояние было введено для устранения ошибки 6801020, из-за которой потоки зависали при одновременном освобождении общих блокировок. (При постоянном совершенствовании JDK его код становится все более сложным для понимания :(, как и наш собственный инженерный код, чем больше исправлено ошибок, тем более неясны детали)
  • 0: исходное состояние

Где ОТМЕНА=1, СИГНАЛ=-1, УСЛОВИЕ=-2, РАСПРОСТРАНЕНИЕ=-3. В конкретной реализации вы можете просто снять состояние ожидания, меньшее или равное 0, чтобы определить, является ли это состоянием CANCELED.

эксклюзивный замок

Поняв основные идеи реализации и структуру данных AbstractQueuedSynchronizer, давайте рассмотрим детали его реализации. Во-первых, давайте посмотрим на реализацию монопольной блокировки. Повторно входящая блокировка — типичная монопольная блокировка.

Запрос замок

Ниже приведен код, который блокирует его для получения лицензии на запрос:

public final void acquire(int arg) {
    //尝试获得许可, arg为许可的个数。对于重入锁来说,每次请求1个。
    if (!tryAcquire(arg) &&
    // 如果tryAcquire 失败,则先使用addWaiter()将当前线程加入同步等待队列
    // 然后继续尝试获得锁
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

Идем дальше и взглянем на функцию tryAcquire(). Эта функция пытается получить разрешение. Для AbstractQueuedSynchronizer это нереализованная абстрактная функция.

Конкретная реализация находится в подклассе. В реализации реентерабельной блокировки, блокировки чтения-записи, семафора и т. д. у каждого своя реализация.

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

Затем используйте функциюAcquireQueued(), чтобы запросить блокировку для потока, уже находящегося в очереди.Как вы можете видеть из имени функции, ее параметр node должен быть узлом, который уже ожидает в очереди. Его функция — запросить разрешение для узла, который уже стоит в очереди.

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

условная переменная ждать

Если вызвать Condition.await(), то поток тоже войдет в ожидание, посмотрим реализацию:

signal() уведомление об объекте Condition

Когда сигнал () уведомлен, он находится в очереди ожидания состояния, в соответствии с FIFO, сначала начните с первого узла:

release() снимает блокировку

Снять монопольную блокировку очень просто

public final boolean release(int arg) {
    //tryRelease()是一个抽象方法,在子类中有具体实现和tryAcquire()一样
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 从队列中唤醒一个等待中的线程(遇到CANCEL的直接跳过)
            unparkSuccessor(h);
            return true;
    }
    return false;
}

общий замок

По сравнению с монопольными блокировками реализация разделяемых блокировок несколько сложнее. Это тоже понятно. Потому что сценарий эксклюзивных блокировок очень прост, одиночные входные и одиночные исходящие, а разделяемые блокировки разные. Это может быть N in и M out, с чем сложнее иметь дело. Однако их основные идеи остаются прежними. Несколько типичных применений разделяемых блокировок: семафоры, блокировки записи в блокировках чтения-записи.

Получить общий замок

Для реализации общих блокировок в AbstractQueuedSynchronizer есть набор методов для общих блокировок.

Чтобы получить общую блокировку, используйте методAcquireShared():

снять общую блокировку

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

public final boolean releaseShared(int arg) {
    //tryReleaseShared()尝试释放许可,这是一个抽象方法,需要在子类中实现
    if (tryReleaseShared(arg)) {
        //上述代码中已经出现这个函数了,就是唤醒线程,设置传播状态
        doReleaseShared();
        return true;
    }
    return false;
}

последние слова

AbstractQueuedSynchronizer — сложная реализация, и для полного понимания деталей требуется время.

Эта статья может сыграть лишь роль привлечения новых идей, показывая всем идею дизайна AbstractQueuedSynchronizer, структуру данных ядра и код реализации ядра. Я надеюсь, что это поможет вам понять реализацию AbstractQueuedSynchronizer, а также повторные блокировки, семафоры и блокировки чтения-записи.

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


Я Ао Бин,Чем больше вы знаете, тем больше вы не знаете, спасибо за ваши таланты:как,собиратьиКомментарий, увидимся в следующий раз!


Статья постоянно обновляется, вы можете искать в WeChat "Третий принц Ао Бин"Прочтите это в первый раз, ответьте [материал] Подготовленные мной материалы интервью и шаблоны резюме крупных заводов первой линии, эта статьяGitHub github.com/JavaFamilyОн был включен, и есть полные тестовые сайты для интервью с крупными заводами.Добро пожаловать в Star.