Многопоточные вопросы для интервью, которые любят задавать BATJ

интервью Java задняя часть JVM

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

Эта статья была добавлена ​​в документ с открытым исходным кодом: JavaGuide (обложка, которая охватывает основные знания, которые необходимо освоить большинству Java-программистов). адрес:GitHub.com/snail Climb/….

Популярные облачные продукты Tencent Cloud начинаются со скидки 10% и получают пакет продления/обновления стоимостью 13 000 юаней:cloud.Tencent.com/redirect. С…

Большой ваучер для нового пользователя Tencent Cloud:cloud.Tencent.com/redirect. С…

5 комбо по синхронизированному ключевому слову в интервью

1.1 Расскажите, как вы понимаете ключевое слово synchronized

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

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

1.2 Расскажите, как вы используете ключевое слово synchronized и использовали ли вы его в своем проекте?

Три основных способа использования ключевого слова synchronized:

  • Модифицированный метод экземпляра, который воздействует на текущий экземпляр объекта для блокировки и получает блокировку текущего экземпляра объекта перед вводом кода синхронизации.
  • Измените статический метод, который воздействует на текущий объект класса для блокировки и получает блокировку текущего объекта класса перед вводом кода синхронизации.. То есть блокировка текущего класса будет действовать на все экземпляры объекта класса, потому что статические члены не принадлежат какому-либо экземпляру объекта, а являются членами класса ( static указывает, что это статический ресурс класса, независимо от того, сколько объектов новые, только один экземпляр, поэтому все объекты класса заблокированы). Таким образом, если поток A вызывает нестатический синхронизированный метод объекта экземпляра, а потоку B необходимо вызвать статический синхронизированный метод класса, к которому принадлежит экземпляр объекта, это разрешено, и взаимное исключение не произойдет.Поскольку блокировка, занимаемая при доступе к статическому синхронизированному методу, является блокировкой текущего класса, а блокировка, занимаемая при доступе к нестатическому синхронизированному методу, является блокировкой объекта текущего экземпляра..
  • Измените блок кода, укажите объект блокировки, заблокируйте данный объект и получите блокировку данного объекта перед входом в базу кода синхронизации.Как и метод synchronized, блок synchronized(this) блокирует текущий объект. Синхронизированное ключевое слово добавляется к статическим статическим методам и блокам кода synchronized(class) для блокировки класса Class. Здесь это снова упоминается: ключевое слово synchronized добавляется к нестатическому статическому методу для блокировки экземпляра объекта. Еще одно замечание: старайтесь не использовать synchronized(String a), потому что в JVM пул строковых констант имеет функцию буферизации!

Ниже я использовал распространенный вопрос из интервью в качестве примера, чтобы объяснить конкретное использование ключевого слова synchronized.

Во время собеседования интервьюер часто говорит: "Вы понимаете режим синглтона? Приходите и напишите его для меня! Объясните мне принцип метода двойной проверки блокировки для реализации режима простого интереса!"

Блокировка с двойной проверкой реализует одноэлементный объект (безопасность потоков)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

Кроме того, необходимо отметить, что uniqueInstance модифицируется ключевым словом volatile.

Также необходимо изменить uniqueInstance с помощью ключевого слова volatile uniqueInstance = new Singleton(); Этот код фактически разделен на три шага:

  1. Выделить место в памяти для uniqueInstance
  2. инициализировать уникальный экземпляр
  3. Укажите uniqueInstance на выделенный адрес памяти

Однако из-за особенностей перестановки команд в JVM порядок выполнения может стать 1->3->2. Перестановка инструкций не вызывает проблем в однопоточной среде, но в многопоточной среде может привести к тому, что поток получит экземпляр, который не был инициализирован. Например, поток T1 выполняет 1 и 3. В это время, после того как T2 вызывает getUniqueInstance(), он обнаруживает, что uniqueInstance не является пустым, поэтому он возвращает uniqueInstance, но uniqueInstance в это время не был инициализирован.

Использование volatile может запретить перестановку инструкций JVM и обеспечить нормальную работу в многопоточной среде.

1.3 Расскажите об основном принципе синхронизированного ключевого слова

Основополагающий принцип синхронизированного ключевого слова относится к уровню JVM.

① Случай синхронизированного синхронизированного блока

public class SynchronizedDemo {
	public void method() {
		synchronized (this) {
			System.out.println("synchronized 代码块");
		}
	}
}

Просмотрите соответствующую информацию о байт-коде класса SynchronizedDemo с помощью команды javap, которая поставляется с JDK: сначала переключитесь в соответствующий каталог класса для выполнения.javac SynchronizedDemo.javaКоманда создает скомпилированный файл .class, а затем выполняетjavap -c -s -v -l SynchronizedDemo.class.

synchronized 关键字原理

Из вышеизложенного мы видим, что:

Реализация блока синхронизированных операторов synchronized использует инструкции monitorenter и monitorexit, где инструкция monitorenter указывает на начало синхронизированного блока кода, а инструкция monitorexit указывает на конец синхронизированного блока кода.Когда выполняется инструкция monitorenter, поток пытается получить блокировку, которая должна получить монитор (объект монитора существует в заголовке объекта каждого объекта Java, и синхронизированная блокировка получает блокировку таким образом, поэтому любой объект в Java можно использовать в качестве блокировки. Причина) имеет право. Когда счетчик равен 0, он может быть успешно получен. После получения счетчик блокировки устанавливается в 1, то есть увеличивается на 1. Соответственно, после выполнения инструкции monitorexit счетчик блокировки устанавливается в 0, указывая на то, что блокировка снята. Если получение блокировки объекта не удается, текущий поток заблокируется и будет ждать, пока блокировка не будет снята другим потоком.

② Случай синхронизированного модифицированного метода

public class SynchronizedDemo2 {
	public synchronized void method() {
		System.out.println("synchronized 方法");
	}
}

synchronized 关键字原理

Синхронизированный модифицированный метод не имеет инструкции monitorenter и инструкции monitorexit. Вместо этого, это действительно флаг ACC_SYNCHRONIZED, который указывает, что метод является синхронизированным методом. JVM использует флаг доступа ACC_SYNCHRONIZED, чтобы определить, объявлен ли метод как синхронизированный метод, чтобы выполнить соответствующий синхронный вызов.

1.4 Скажите, какие оптимизации были сделаны в нижней части ключевого слова synchronized после JDK1.6. Можете ли вы подробно рассказать об этих оптимизациях?

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

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

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

1.5 Разговор о разнице между синхронизированным и ReenTrantLock

① Обе блокировки являются реентерабельными.

Обе блокировки являются реентерабельными. Концепция «повторно используемой блокировки» заключается в том, что вы можете снова получить свою собственную внутреннюю блокировку. Например, поток получает блокировку объекта, но блокировка объекта в это время не снята. Когда он захочет снова получить блокировку этого объекта, он все еще может быть получен. Если он не может быть заблокирован и повторно входить, это вызовет тупик. Каждый раз, когда один и тот же поток получает блокировку, счетчик блокировок увеличивается на 1, поэтому блокировку нельзя снять, пока счетчик блокировок не упадет до 0.

② синхронизация зависит от JVM, а ReenTrantLock зависит от API

Synchronized зависит от реализации JVM.Мы также упоминали ранее, что команда виртуальной машины сделала много оптимизаций для синхронизированного ключевого слова в JDK1.6, но эти оптимизации были реализованы на уровне виртуальной машины и не были представлены нам напрямую. ReenTrantLock реализован на уровне JDK (то есть на уровне API, для которого требуются методы блокировки() и разблокировки с блоками операторов try/finally), поэтому мы можем посмотреть на его исходный код, чтобы увидеть, как он реализован.

③ ReenTrantLock добавляет некоторые расширенные функции по сравнению с синхронизированным

По сравнению с синхронизированным, ReenTrantLock добавляет некоторые дополнительные функции. Есть три основных момента:① Ожидание может быть прервано; ② Достигнута справедливая блокировка; ③ Можно добиться выборочного уведомления (блокировки могут быть привязаны к нескольким условиям)

  • ReenTrantLock предоставляет механизм для прерывания потоков, ожидающих блокировки., который реализует этот механизм через lock.lockInterruptably(). Другими словами, ожидающий поток может отказаться от ожидания и вместо этого заняться другими делами.
  • ReenTrantLock может указать, является ли блокировка честной или несправедливой. А синхронизироваться может только несправедливая блокировка. Так называемая справедливая блокировка заключается в том, что поток, ожидающий первым, получает блокировку первым.ReenTrantLock по умолчанию является несправедливым, и к нему можно получить доступ через класс ReenTrantLock.ReentrantLock(boolean fair)Конструктор формулирует, справедливо ли это.
  • Ключевое слово synchronized в сочетании с методами wait() и notify/notifyAll() может реализовать механизм ожидания/уведомления.Конечно, можно реализовать и класс ReentrantLock, но он должен опираться на интерфейс Condition и метод newCondition(). . Условие доступно только после JDK1.5.Он обладает хорошей гибкостью.Например, он может реализовать функцию многоканального уведомления, то есть в объекте блокировки может быть создано несколько экземпляров условия (т.е. мониторы объектов).Объект потока может быть зарегистрирован в указанном состоянии, так что уведомление потока может быть выборочно выполнено, что более гибко в планировании потоков. При использовании метода Notify / Notibleall () для уведомления, уведомленный нить выбран JVM, а класс ReentrantLock в сочетании с экземпляром состояния может достичь «селективного уведомления», эта функция очень важна и по умолчанию предоставляется интерфейсом Condition. Синхронизированное ключевое слово эквивалентно только одному экземпляру Condition во всем объекте Lock, и все потоки регистрируются в нем. Если метод notifyAll() будет выполнен, все ожидающие потоки будут уведомлены, что вызовет большие проблемы с эффективностью, а метод signalAll() экземпляра Condition только разбудит все ожидающие потоки, зарегистрированные в экземпляре Condition.

Если вы хотите использовать вышеуказанные функции, то выбор ReenTrantLock — хороший выбор.

④ Производительность больше не является критерием выбора

Два 4 комбо о пулах потоков в интервью

2.1 Разговор о модели памяти Java

До JDK1.2 реализация модели памяти Java всегда начиналась сосновная память(то есть общая память) чтение переменных не требует особого внимания. В текущей модели памяти Java потоки могут сохранять переменныелокальная память(например, машинные регистры) вместо чтения и записи непосредственно в основную память. Это может привести к тому, что один поток изменит значение переменной в основной памяти, в то время как другой поток продолжит использовать свою копию значения переменной в регистре, что приведет кнесогласованность данных.

数据的不一致

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

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

volatile关键字的可见性

2.2 Поговорите о разнице между ключевым словом synchronized и ключевым словом volatile

Сравнение синхронизированных ключевых слов и изменчивых ключевых слов

  • изменчивое ключевое словосинхронизируется с потокомЛегкая реализация,такvolatile производительность определенно лучше, чем синхронизированное ключевое слово. ноКлючевое слово volatile может использоваться только для переменных, а ключевое слово synchronized может изменять методы и блоки кода.. После JavaSE1.6 ключевое слово synchronized в основном включает предвзятые блокировки и облегченные блокировки, введенные для снижения потребления производительности, вызванного получением и снятием блокировок, а также другие различные оптимизации, и эффективность выполнения была значительно улучшена.Существует больше сценариев, в которых ключевое слово synchronized используется в фактической разработке..
  • Многопоточный доступ к ключевому слову volatile не блокируется, а ключевое слово synchronized может блокироваться.
  • Ключевое слово volatile может гарантировать видимость данных, но не может гарантировать атомарность данных. Синхронизированное ключевое слово гарантирует и то, и другое.
  • Ключевое слово volatile в основном используется для обеспечения видимости переменных между несколькими потоками, а ключевое слово synchronized решает проблему синхронизации ресурсов доступа между несколькими потоками.

2 комбо о пулах потоков в трех интервью

3.1 Зачем использовать пул потоков?

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

Вот преимущества использования пулов потоков, упомянутые в статье «Искусство параллельного программирования на Java»:

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

3.2 Разница между реализацией интерфейса Runnable и интерфейса Callable

Если вы хотите, чтобы пул потоков выполнял задачи, вам необходимо реализовать интерфейс Runnable или интерфейс Callable. Класс реализации интерфейса Runnable или Callable может выполняться ThreadPoolExecutor или ScheduledThreadPoolExecutor. Разница между ними заключается в том, что интерфейс Runnable не возвращает результатов, а интерфейс Callable может возвращать результаты.

Примечание:ИнструментыExecutorsможет быть реализованRunnableобъект иCallableВзаимопреобразование между объектами. (Executors.callable(Runnable task)илиExecutors.callable(Runnable task,Object resule)).

3.3 В чем разница между методом execute() и методом submit()?

1)execute()Метод используется для отправки задач, не требующих возвращаемого значения, поэтому невозможно определить, успешно выполняется задача пулом потоков или нет;

2)Метод submit() используется для отправки задач, требующих возвращаемого значения. Пул потоков вернет объект типа future, с помощью которого можно использовать объект future, чтобы определить, успешно ли выполнена задача., а возвращаемое значение можно получить с помощью метода get() в будущем. Метод get() заблокирует текущий поток до тех пор, пока задача не будет завершена, и используетget(long timeout,TimeUnit unit)Метод заблокирует текущий поток на некоторое время и немедленно вернется.В это время возможно, что задача не была выполнена.

3.4 Как создать пул потоков

В «Руководстве по разработке Java для Alibaba» обязательный пул потоков не позволяет использовать исполнителей для создания, а использует метод ThreadPoolExecutor. Этот метод обработки позволяет учащимся более четко писать правила работы пула потоков и избегать риска исчерпания ресурсов**

Недостатки Executors, возвращающие объекты пула потоков, заключаются в следующем:

  • FixedThreadPool и SingleThreadExecutor: длина очереди разрешенных запросов — Integer.MAX_VALUE, что может привести к скоплению большого количества запросов, что приведет к OOM.
  • CachedThreadPool и ScheduledThreadPool: допустимое количество потоков — Integer.MAX_VALUE , может быть создано большое количество потоков, что приведет к OOM.

Способ 1: Реализуется методом построения

通过构造方法实现
Способ 2: Реализован через класс инструментов Executors фреймворка Executor.Мы можем создать три типа ThreadPoolExecutor:

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

Методы в соответствующем инструментальном классе Executors показаны на рисунке:

通过Executor 框架的工具类Executors来实现

4 комбо на атомном классе в интервью

4.1 Введение в атомный класс Atomic

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

Следовательно, так называемый атомарный класс — это просто класс с атомарными/атомарными характеристиками операций.

параллельный пакетjava.util.concurrentАтомарные классы хранятся вjava.util.concurrent.atomicвниз, как показано на рисунке ниже.

JUC 原子类概览

4.2 Какие четыре типа атомарных классов есть в пакете JUC?

базовый тип

Обновление примитивных типов атомарно

  • AtomicInteger: целочисленный атомарный класс
  • AtomicLong: длинный целочисленный атомарный класс
  • AtomicBoolean : логический атомарный класс

тип массива

Обновить элемент в массиве атомарно

  • AtomicIntegerArray: атомарный класс целочисленного массива
  • AtomicLongArray: атомарный класс для массивов длинных целых чисел.
  • AtomicReferenceArray: атомарный класс массива ссылочных типов

тип ссылки

  • AtomicReference: атомарный класс ссылочного типа
  • AtomicStampedRerence: атомарное поле обновления атомарного класса в ссылочном типе
  • AtomicMarkableReference : атомарное обновление ссылочного типа с маркерными битами.

Тип модификации свойства объекта

  • AtomicIntegerFieldUpdater: средство обновления, которое атомарно обновляет целочисленные поля.
  • AtomicLongFieldUpdater: средство обновления, которое атомарно обновляет длинные поля.
  • AtomicStampedReference : Atomic обновляет ссылочный тип номером версии. Этот класс связывает целочисленное значение со ссылкой и может использоваться для разрешения данных атомарного обновления и номеров версий данных, что может решить проблемы ABA, которые могут возникнуть при использовании CAS для атомарных обновлений.

4.3 Расскажите об использовании AtomicInteger

Общие методы класса AtomicInteger

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

Пример использования класса AtomicInteger

После использования AtomicInteger можно гарантировать потокобезопасность без блокировки метода increment().

class AtomicIntegerTest {
        private AtomicInteger count = new AtomicInteger();
      //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。
        public void increment() {
                  count.incrementAndGet();
        }
     
       public int getCount() {
                return count.get();
        }
}

4.4 Не могли бы вы дать мне краткое введение в принцип класса AtomicInteger?

Простой анализ принципа безопасности потоков AtomicInteger

Часть исходного кода класса AtomicInteger:

    // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

Класс AtomicInteger в основном использует CAS (сравнение и обмен) + volatile и собственные методы для обеспечения атомарных операций, тем самым избегая высоких накладных расходов на синхронизацию и значительно повышая эффективность выполнения.

Принцип CAS заключается в сравнении ожидаемого значения с исходным значением и обновлении его до нового значения, если оно совпадает. Метод objectFieldOffset() класса UnSafe является собственным методом, который используется для получения адреса памяти «исходного значения», а возвращаемое значение — valueOffset. Кроме того, значение — это изменчивая переменная, видимая в памяти, поэтому JVM может гарантировать, что любой поток всегда может получить самое последнее значение переменной в любое время.

Для получения дополнительной информации об атомных классах Atomic вы можете проверить эту мою статью: Concurrent Programming Interview Essentials:Сводка атомных атомных классов в JUC

Пять AQS

5.1 Введение в AQS

Полное имя AQS (AbstractQueuedSynchronizer), этот класс находится в пакете java.util.concurrent.locks.

enter image description here

AQS — это платформа для создания блокировок и синхронизаторов.Использование AQS позволяет легко и эффективно создавать большое количество широко используемых синхронизаторов, таких как ReentrantLock, Semaphore, о котором мы упоминали, и другие, такие как ReentrantReadWriteLock, SynchronousQueue, FutureTask и т. д. на АКС. Конечно, мы также можем использовать AQS для простого создания синхронизатора, отвечающего нашим потребностям.

5.2 Принципиальный анализ AQS

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

Когда на собеседовании их спрашивают о параллельных знаниях, большинству из них задают вопрос: «Пожалуйста, расскажите мне, как вы понимаете принцип AQS». Ниже приведен пример, в котором может принять участие каждый. Интервью — это не заучивание. Вы должны исходить из своих собственных мыслей. Даже если вы не можете добавить свои собственные мысли, вы должны убедиться, что вы можете высказать их популярно, а не повторять Это.

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

5.2.1 Обзор принципа AQS

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

Очередь CLH (Craig, Landin, and Hagersten) — это виртуальная двусторонняя очередь (виртуальная двусторонняя очередь означает отсутствие экземпляра очереди, а только связь между узлами). AQS инкапсулирует каждый поток, запрашивающий общие ресурсы, в узел (Node) очереди блокировок CLH для обеспечения выделения блокировок.

Посмотрите на принципиальную схему AQS (AbstractQueuedSynchronizer):

enter image description here

AQS использует переменную-член int для представления состояния синхронизации и завершает создание очереди потоков получения ресурсов через встроенную очередь FIFO. AQS использует CAS для выполнения атомарных операций с состоянием синхронизации для изменения его значения.

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

Информация о состоянии обрабатывается через getState, setState, compareAndSetState типа procted.


//返回同步状态的当前值
protected final int getState() {  
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

5.2.2 Как AQS распределяет ресурсы

AQS определяет два способа совместного использования ресурсов

  • Exclusive(Эксклюзив): может выполняться только один поток, например ReentrantLock. Его можно разделить на честный замок и несправедливый замок:
    • Справедливая блокировка: в соответствии с очередностью потоков в очереди первый встречный получает блокировку первым.
    • Несправедливые блокировки: когда поток хочет получить блокировку, он захватывает блокировку напрямую, независимо от порядка очереди, и тот, кто ее захватывает, тот, кто ее захватывает.
  • Share(Общий): несколько потоков могут выполняться одновременно, например Semaphore/CountDownLatch. Semaphore, CountDownLatCh, CyclicBarrier, ReadWriteLock будут рассмотрены позже.

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

Различные пользовательские синхронизаторы конкурируют за общие ресурсы по-разному. При реализации пользовательского синхронизатора вам нужно только реализовать методы получения и освобождения состояния общего ресурса.Что касается обслуживания конкретного потока, ожидающего очереди (например, невозможность получения ресурсов для входа в очередь/пробуждение и dequeue и т. д.), AQS был реализован на верхнем уровне.

5.2.3 Нижний уровень AQS использует шаблон метода шаблона

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

  1. Потребитель наследует AbstractQueuedSynchronizer и переопределяет указанный метод. (Эти методы переопределения очень просты, не более чем получение и освобождение состояния общего ресурса)
  2. Объедините AQS с реализацией пользовательского компонента синхронизации и вызовите его методы шаблона, которые вызывают методы, переопределяемые потребителем.

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

AQS использует шаблон метода шаблона.При настройке синхронизатора необходимо переписать следующие методы шаблона, предоставляемые AQS:

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

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

Взяв в качестве примера ReentrantLock, состояние инициализируется равным 0, что указывает на разблокированное состояние. Когда поток A блокирует(), он вызывает функцию tryAcquire(), чтобы монополизировать блокировку и состояние+1. После этого другие потоки будут терпеть неудачу, когда они снова попытаются получить доступ к функции Acquire().Пока поток A не разблокирует() до состояния=0 (то есть не снимет блокировку), у других потоков нет шансов получить блокировку. Конечно, прежде чем снять блокировку, поток A может повторно ее получить (состояние будет накапливаться), что является концепцией повторного входа. Но обратите внимание на то, сколько раз вам нужно его отпустить, чтобы убедиться, что состояние может вернуться в нулевое состояние.

Взяв в качестве примера CountDownLatch, задача делится на N подпотоков для выполнения, и состояние также инициализируется равным N (обратите внимание, что N должно соответствовать количеству потоков). N подпотоков выполняются параллельно.После выполнения каждого подпотока countDown() выполняется один раз, и состояние уменьшится на 1 в CAS (Сравнить и поменять местами). После выполнения всех дочерних потоков (т. е. состояние = 0) основным вызывающим потоком будет unpark(), а затем основной вызывающий поток вернется из функции await() и продолжит остальную часть действия.

Как правило, пользовательские синхронизаторы являются либо эксклюзивными, либо общими, и им нужно только реализоватьtryAcquire-tryRelease,tryAcquireShared-tryReleaseSharedМожно использовать один из них. Но AQS также поддерживает пользовательские синхронизаторы для одновременной реализации как эксклюзивных, так и общих методов, напримерReentrantReadWriteLock.

Рекомендуются две статьи о принципах AQS и соответствующем анализе исходного кода:

5.3 Обзор компонентов AQS

  • Semaphore (семафор) — позволяет нескольким потокам обращаться одновременно:И синхронизированный, и ReentrantLock позволяют только одному потоку обращаться к ресурсу за раз, а Semaphore (семафор) может указывать нескольким потокам доступ к ресурсу одновременно.
  • CountDownLatch (таймер обратного отсчета):CountDownLatch — это класс инструментов синхронизации, используемый для координации синхронизации между несколькими потоками. Этот инструмент обычно используется для управления ожиданием потока, он может заставить поток ждать, пока не закончится обратный отсчет, а затем начать выполнение.
  • Циклический барьер:CyclicBarrier очень похож на CountDownLatch, он также может реализовывать техническое ожидание между потоками, но его функция более сложная и мощная, чем CountDownLatch. Основные сценарии применения аналогичны CountDownLatch. CyclicBarrier буквально означает перерабатываемый (Циклический) барьер (Барьер). Что он делает, так это блокирует группу потоков, когда они достигают барьера (также называемого точкой синхронизации), до тех пор, пока последний поток не достигнет барьера, барьер откроет дверь, и все потоки, заблокированные барьером, продолжат работу. Конструктор CyclicBarrier по умолчанию — CyclicBarrier(int party), а его параметр указывает количество потоков, перехваченных барьером.Каждый поток вызывает метод await, чтобы сообщить CyclicBarrier, что я достиг барьера, после чего текущий поток блокируется.

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

Reference

【настоятельно рекомендуется! Не реклама! ] Alibaba Cloud Double 11 матрасов и шерсти (10.29-11.12):Aliyun.com/act/team111…. Анализ события в одном предложении: Новые пользователи могут купить со скидкой 10% (1-ядерный 2g сервер стоит всего 8,3/мес, что дешевле студенческих компьютеров. Очень настоятельно рекомендуется остаться на 3 года). Старые пользователи могут присоединиться к моей команде, а затем поделиться своими ссылками, чтобы получить красные конверты и 25% кэшбэка.На данный момент в нашей команде 300 новичков, поэтому мы можем войти в топ-100, а затем мы можем разделить миллионы наличных денег (согласно количество новобранцев) Делите кеш, чем больше вытянете, тем больше очков! Не создавайте новую команду в одиночку, вы не сможете потом участвовать в кешшеринге).

Если ты расцветешь, ветерок придет. Добро пожаловать в мою общедоступную учетную запись WeChat: «Руководство по прохождению интервью на Java», теплую общедоступную учетную запись WeChat. Ответьте на ключевое слово "1" в фоне официального аккаунта, и вы сможете бесплатно получить небольшой подарок, который я тщательно подготовил!