16 вопросов об основах Java в книге «Я хочу работать на большом заводе»

Java
16 вопросов об основах Java в книге «Я хочу работать на большом заводе»

Было сказано, что серия интервью закончилась, а оказалось, что она еще очень ароматна. Ну, я думал, что обнаружил, что не писал основы Java, поэтому это считается продолжением. Пожалуйста, сохраните первое продолжение. .

В чем разница между процессом и потоком?

Процесс - это выполнение программы и независимая единица для распределения ресурсов и планирования в системе. Его функция состоит в том, чтобы позволить программам выполняться одновременно, чтобы улучшить использование ресурсов и пропускную способность.

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

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

Вы знаете принцип синхронизации?

Synchronized – это атомарная встроенная блокировка, предоставляемая java. Эта встроенная блокировка, невидимая для пользователя, также называетсяблокировка монитора, После использования инструкций байт-кода synchronized, monitorenter и monitorexit будут добавлены до и после синхронизированного блока кода после компиляции, что зависит от базовой реализации блокировки мьютекса в операционной системе. Его роль в основном заключается в выполнении атомарных операций и решении проблемы видимости памяти общих переменных.

Когда инструкция monitorenter выполняется, она попытается получить блокировку объекта.Если объект не заблокирован или блокировка была получена, счетчик блокировки +1. В это время другие потоки, конкурирующие за блокировку, войдут в очередь ожидания.

При выполнении инструкции monitorexit счетчик будет равен -1.Когда значение счетчика равно 0, блокировка будет снята, и потоки в очереди ожидания продолжат борьбу за блокировку.

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

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

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

Если углубиться в исходный код, synchronized на самом деле имеет две очереди waitSet и entryList.

  1. Когда несколько потоков входят в блок синхронизированного кода, сначала вводится список entryList.
  2. После того, как поток получает блокировку монитора, он назначает ее текущему потоку, а счетчик +1
  3. Если поток вызовет метод ожидания, блокировка будет снята, текущий поток будет установлен в нуль, счетчик будет равен -1, и в то же время он войдет в режим ожидания и будет ждать пробуждения. notifyAll, он войдет в entryList для борьбы за блокировку.
  4. Если поток выполняется, блокировка также снимается, счетчик равен -1, а текущий поток устанавливается равным нулю.

Вы понимаете механизм оптимизации блокировки?

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

Состояние замков от низкого к высокому следующееБез блокировки -> Блокировка смещения -> Легкая блокировка -> Тяжелая блокировка, процесс обновления идет от низкого к высокому, а также возможен даунгрейд при определенных условиях.

блокировка спина: Поскольку большую часть времени блокировка занята на очень короткое время, и время блокировки общей переменной также очень короткое, поэтому нет необходимости приостанавливать поток, и переключение контекста туда и обратно между пользовательский режим и режим ядра серьезно влияют на производительность. Концепция вращения заключается в том, чтобы позволить потоку выполнить цикл занятости. Это можно понимать как ничего не делать для предотвращения перехода из пользовательского состояния в состояние ядра. Блокировку вращения можно включить, установив -XX:+UseSpining. Номер по умолчанию спинов в 10 раз, вы можете использовать параметр -XX:PreBlockSpin.

Адаптивная блокировка: Адаптивная блокировка - это адаптивная блокировка вращения.Время вращения не является фиксированным временем, а определяется предыдущим временем вращения на том же замке и состоянием держателя замка.

снятие блокировки: Устранение блокировки означает, что JVM обнаруживает некоторые синхронизированные блоки кода, а сценарий конкуренции данных вообще отсутствует, то есть устранение блокировки выполняется без блокировки.

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

Блокировка смещения: когда поток обращается к синхронизированному блоку для получения блокировки, идентификатор потока смещенной блокировки будет сохранен в записи блокировки в заголовке объекта и кадре стека.После того, как этот поток снова входит в синхронизированный блок, ему не требуется CAS для блокировка и разблокировка, смещенная блокировка. Он всегда будет смещен в сторону первого потока, который получает блокировку. Если ни один другой поток не получит блокировку в будущем, поток, удерживающий блокировку, никогда не будет нуждаться в синхронизации. Наоборот, когда есть другие потоки, конкурирующие за блокировку смещения, поток, удерживающий блокировку смещения, будет снят. Предвзятую блокировку можно включить, установив -XX:+UseBiasedLocking.

Легкий замок: Заголовок объекта JVM содержит некоторые флаги блокировки. Когда код входит в синхронизированный блок, JVM будет использовать метод CAS, чтобы попытаться получить блокировку. Если обновление прошло успешно, бит состояния в заголовке объекта будет помечен как облегченная блокировка уровня, если обновление не удается, текущий поток пытается получить блокировку.

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

Проще говоря, смещенная блокировка сравнивается по смещенному идентификатору потока заголовка объекта, и CAS даже не нужен, в то время как облегченная блокировка в основном реализуется путем модификации записи блокировки заголовка объекта и вращения через CAS, а тяжеловесная блокировка является дополнением к блокировке всех других потоков, владеющих блокировкой.

Что именно содержит заголовок объекта?

В нашей часто используемой виртуальной машине Hotspot расположение объектов в памяти фактически состоит из 3 частей:

  1. заголовок объекта
  2. данные экземпляра
  3. Выровнять отступы

Заголовок объекта состоит из двух частей, и содержимое в Mark Word будет меняться вместе с битом флага блокировки, так что просто поговорим о структуре хранения.

  1. Данные, необходимые для запуска самого объекта, также известные как Mark Word, являются ключевым моментом для облегченных блокировок и необъективных блокировок. Конкретное содержимое включает хэш-код объекта, возраст поколения, указатель облегченной блокировки, указатель тяжелой блокировки, метку GC, идентификатор потока предвзятой блокировки и временную метку предвзятой блокировки.
  2. Указатель типа хранилища, то есть указатель на метаданные класса, можно использовать для определения экземпляра, к какому классу принадлежит объект.

Если это массив, он также включает длину массива

Что касается блокировки, как насчет принципа ReentrantLock? Чем он отличается от синхронизированного?

По сравнению с синхронизированным, ReentrantLock требует явного получения и освобождения блокировок.По сравнению с версиями JDK7 и JDK8, эффективность ReentrantLock и синхронизированного может быть в основном одинаковой. Их основные отличия заключаются в следующем:

  1. Ожидание может быть прервано.Когда поток, удерживающий блокировку, не освобождает блокировку в течение длительного времени, ожидающий поток может отказаться от ожидания и вместо этого обработать другие задачи.
  2. Справедливая блокировка: и синхронизированная, и ReentrantLock по умолчанию являются несправедливыми блокировками, но ReentrantLock можно изменить, передав параметры через конструктор. Однако использование справедливых блокировок приведет к резкому падению производительности.
  3. Связывание нескольких условий: ReentrantLock может одновременно связывать несколько объектов условий Condition.

ReentrantLock основан на AQS (AbstractQueuedSynchronizer абстрактный синхронизатор очереди)выполнить. Не говорите, я знаю проблему, позвольте мне объяснить принцип AQS.

AQS поддерживает внутренний бит состояния. При попытке блокировки измените значение через CAS (CompareAndSwap). Если он успешно установлен в 1 и назначен текущий идентификатор потока, это означает, что блокировка выполнена успешно. После получения блокировки , другие потоки будут Заблокированный поток входит в очередь блокировки и вращается. Когда поток, который получает блокировку, освобождает блокировку, он пробуждает поток в очереди блокировки. Когда блокировка снимается, состояние будет сброшено на 0, и текущий идентификатор потока будет пустым.

Как насчет принципа CAS?

CAS называется CompareAndSwap, сравнение и обмен, в основном через инструкции процессора для обеспечения атомарности операции, содержит три операнда:

  1. Переменный адрес памяти, V представляет
  2. старое математическое ожидание, A для
  3. Новое значение, которое необходимо установить, B означает

Когда выполняется инструкция CAS, только когда V равно A, B будет использоваться для обновления значения V, в противном случае операция обновления не будет выполнена.

Так есть ли недостатки у CAS?

У CAS есть три основных недостатка:

АВА-проблема: Проблема ABA связана с тем, что в процессе обновления CAS, когда считанное значение равно A, а затем оно готово к присвоению, это все еще A, но на самом деле возможно, что значение A было изменился на B, а затем снова изменился.Возвращаясь к A, эта уязвимость обновления CAS называется ABA. Просто проблема ABA не влияет на конечный эффект параллелизма в большинстве сценариев.

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

Длительное время цикла и высокие накладные расходы: Если метод spin CAS будет безуспешным в течение длительного времени, это принесет много накладных расходов на ЦП.

Только гарантированные атомарные операции над общей переменной: Только одна операция с общей переменной может гарантировать атомарность, но несколько операций не могут быть выполнены.Множественные операции могут быть обработаны через AtomicReference или реализованы с использованием синхронизации блокировки.

Хорошо, давайте поговорим о принципе HashMap?

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

поставить вставить поток данных

При вставке элемента в карту сначала выполните операцию И с хэшем ключа, а затем с массивом length-1 ((n-1)&hash), все из которых являются степенями 2, поэтому это эквивалентно взятию по модулю, но эффективность битовой операции выше. После нахождения позиции в массиве, если в массиве нет элемента, он сразу сохраняется, в противном случае судят, совпадает ли ключ, и ключ тот же, он будет перезаписан, иначе будет вставлен в конец связанного списка, если длина связанного списка превышает 8, он будет преобразован в красное и черное дерево и, наконец, определит, превышает ли длина массива длину * коэффициент загрузки по умолчанию, который равен 12, и расширить емкость, если она превышает.

получить данные запроса

Запрос данных относительно прост. Сначала вычислите значение хэша, а затем перейдите к массиву для запроса. Если это красно-черное дерево, перейдите к красно-черному дереву для проверки, и связанный список можно пройти, пройдя связанный список.

Процесс расширения размера

Процесс расширения заключается в пересчете хэша для ключа, а затем копировании данных в новый массив.

Как использовать Map в многопоточной среде? Вы поняли ConcurrentHashmap?

В многопоточной среде для синхронизации блокировки можно использовать Collections.synchronizedMap, также можно использовать HashTable, но метод синхронизации явно не соответствует стандартам производительности, а ConurrentHashMap больше подходит для сценариев с высоким параллелизмом.

ConcurrentHashmap сильно изменился в версиях JDK1.7 и 1.8.1.7 использует блокировку сегмента Segment+HashEntry для реализации, 1.8 отказывается от Segment и вместо этого использует CAS+synchronized+Node, а также добавляет красно-черное дерево, чтобы избежать Связанный список слишком длинный вызвать проблемы с производительностью.

1.7 Блокировка сегмента

Конструктивно версия 1.7 ConcurrentHashMap использует механизм сегментированной блокировки, который содержит массив сегментов, наследуемых от ReentrantLock, а сегменты содержат массив HashEntry. Сам HashEntry представляет собой структуру связанного списка, которая может хранить ключи и значения. , Укажите на Указатель на следующий узел.

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

положить процесс

На самом деле оказалось, что весь процесс очень похож на HashMap, за исключением того, что конкретный сегмент сначала находится, а затем управляется через ReentrantLock.Я упростил последний процесс, потому что он в основном такой же, как HashMap.

  1. Вычислите хэш, найдите сегмент и инициализируйте сегмент, если он пуст.
  2. Используйте ReentrantLock для блокировки. Если блокировка не может быть получена, попробуйте выполнить вращение. Если вращение превышает количество раз, получение будет заблокировано, чтобы гарантировать, что блокировка должна быть успешно получена.
  3. Обход HashEntry такой же, как и у HashMap. Ключ в массиве заменяется так же, как и хеш. Если он не существует, он будет вставлен в связанный список. Связанный список такой же.

получить процесс

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

1.8CAS+synchronized

1.8 Блокировка заброшенных сегментов и переход на CAS+синхронизировано для реализации.Так же HashEntry был изменен на Node, а также добавлена ​​реализация красно-черного дерева. В основном зависит от поставленного процесса.

положить процесс

  1. Сначала вычислите хеш, пройдитесь по массиву узлов, если узел пуст, инициализируйте его с помощью CAS+spin.
  2. Если текущая позиция массива пуста, записывайте данные напрямую через CAS spin
  3. Если hash==MOVED, это означает, что требуется расширение, и расширение выполняется.
  4. Если они не удовлетворены, используйте синхронизированный для записи данных и записывайте данные для оценки связанного списка и красно-черного дерева.Связанный список записывается так же, как HashMap, и хэш ключа перезаписывается.В противном случае вставка хвоста используется метод красно-черного дерева

получить запрос

получить очень просто.Вычислить хэш по ключу.Если хеш ключа совпадает, вернуть его.Если красно-черное дерево получено по красно-черному дереву, то оно не получено путем обхода связного списка.

Вы знаете принцип изменчивости?

По сравнению с методом синхронизированной блокировки для решения проблемы видимости памяти общих переменных volatile является более легким выбором и не требует дополнительных накладных расходов на переключение контекста. Использование переменной, объявленной как volatile, гарантирует, что при обновлении значения оно сразу же станет видимым для других потоков. Volatile использует барьеры памяти, чтобы предотвратить переупорядочивание инструкций, решая проблему видимости памяти.

Мы знаем, что потоки считывают общие переменные из основной памяти в рабочую память для работы, а затем после завершения записывают результаты в основную память, но это вызовет проблемы с видимостью. Например, предположим, что теперь у нас двухъядерная архитектура процессора с двухуровневым кэшем, включая двухуровневый кэш L1 и L2.

  1. Поток A сначала получает значение переменной X. Поскольку первые два уровня кеша пусты, X считывается непосредственно из основной памяти. Предполагая, что начальное значение X равно 0, поток A изменяет значение X на 1 после чтения , и пишет при этом обратно в основную память. В это время кэш и основная память выглядят так, как показано на рисунке ниже.

  1. Поток B также считывает значение переменной X. Поскольку кэш L2 уже имеет кэш X = 1, он читает напрямую из кэша L2, после чего поток B изменяет X на 2 и одновременно записывает обратно в L2 и основную память. . Значение X в это время показано на рисунке ниже.

    Тогда, если поток A захочет снова получить значение переменной X, потому что в кэше L1 уже есть x=1, в это время возникает проблема невидимости переменной памяти, и значение B, измененное на 2, не воспринимается A.

    image-20201111171451466

Затем, если переменная X изменена с помощью volatile, когда поток A снова читает переменную X, ЦП заставит поток A перезагрузить последнее значение из основной памяти в свою собственную рабочую память в соответствии с протоколом когерентности кэша, а не напрямую. используя значение кеша в .

А затем для проблемы барьера памяти после летучих модификации добавит другой барьер памяти, чтобы обеспечить видимость вопроса правильно реализована. Барьер здесь, чтобы написать книгу на основе предоставленного контента, но на самом деле из-за различных архитектур CPU различных повторных политик, предоставляют барьеры памяти не совпадают, например, платформа X86, только для хранения барьера памяти. Отказ

  1. Барьер StoreStore, гарантирующий, что вышеописанные обычные операции записи не будут переупорядочены с помощью энергозависимых записей.
  2. Барьер StoreLoad, гарантирующий, что изменчивые записи и последующие возможные изменчивые операции чтения и записи не будут переупорядочиваться.
  3. Барьер LoadLoad, запрещает переупорядочивание чтения volatile и последующего обычного чтения.
  4. Барьер LoadStore, который запрещает изменчивое чтение и последующее нормальное изменение порядка записи

Итак, расскажите о своем понимании модели памяти JMM? Зачем вам нужен ЖММ?

Из-за разницы в скорости развития ЦП и памяти скорость ЦП намного выше скорости памяти, поэтому текущий ЦП добавил кеш, а кеш вообще можно разделить на L1, L2 и трехуровневые кэши L3. Исходя из вышеприведенного примера, мы знаем, что это приводит к проблеме когерентности кеша, поэтому добавляется протокол когерентности кеша, что также приводит к проблеме видимости памяти, а переупорядочение компилятора и ЦП приводит к атомарности и упорядоченности. Проблема в том, что модель памяти JMM представляет собой ряд нормативных ограничений на многопоточные операции, потому что невозможно, чтобы код сотрудника Чена был совместим со всеми процессорами.Благодаря JMM мы скрываем различия в доступе между различным оборудованием и памятью операционной системы. , чтобы обеспечить Это гарантирует, что Java-программы могут достигать согласованных эффектов доступа к памяти на разных платформах, и в то же время гарантировать, что программы могут выполняться правильно при эффективном параллельном выполнении.

атомарность: Модель памяти Java обеспечивает атомарные операции посредством чтения, загрузки, назначения, использования, сохранения и записи.Кроме того, существуют блокировки и разблокировки, которые напрямую соответствуют инструкциям байт-кода monitorenter и monitorexit ключевого слова synchronized.

видимость: Вопрос о видимости уже упоминался в приведенном выше ответе, Java гарантирует, что видимость можно считать достигнутой за счет volatile, synchronized и final.

упорядоченность: из-за проблемы с порядком, вызванной изменением порядка процессора и компилятора, Java гарантируется изменчивым и синхронизированным.

правило «происходит до»

Хотя перестановка инструкций улучшает производительность параллелизма, виртуальная машина Java накладывает некоторые правила на перестановку инструкций и не может позволить всем инструкциям изменять позицию выполнения по желанию.Основные моменты заключаются в следующем:

  1. Каждая операция в отдельном потоке происходит до любых последующих операций в потоке.
  2. Нестабильные записи происходят до и после чтения этой переменной
  3. синхронизированные разблокировки происходят перед последующим блокированием этого замка
  4. Запись конечных переменных происходит до чтения конечных объектов предметной области, а последующее чтение конечных переменных происходит до
  5. Транзитивное правило: A предшествует B, B предшествует C, тогда A должно произойти до C.

После долгого разговора, что такое рабочая память и основная память?

Оперативную память можно рассматривать как физическую память, а модель памяти Java фактически является частью памяти виртуальной машины. Рабочая память - это кеш процессора, это может быть регистр или кеш L1\L2\L3, это возможно.

Расскажите о принципе ThreadLocal?

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

ThreadLocal имеет статический внутренний класс ThreadLocalMap, а ThreadLocalMap содержит массив Entry. Entry сама по себе является слабой ссылкой, а ее ключ является слабой ссылкой на ThreadLocal. Entry может сохранять пары ключ-значение.

Целью слабых ссылок является предотвращение утечек памяти.Если это сильная ссылка, объект ThreadLocal не может быть повторно использован, пока поток не завершится, а слабая ссылка будет повторно использована при следующем сборщике мусора.

Однако проблема с утечкой памяти по-прежнему будет иметь место.Если ключ и объекты ThreadLocal будут переработаны, будет объект записи с нулевым ключом и значением в записи, но к нему никогда не будет доступа, пока поток не завершит выполнение.

Но пока ThreadLocal используется правильно, вызов метода удаления для удаления объекта Entry после использования на самом деле не вызовет этой проблемы.

Какие бывают ссылочные типы? Какая разница?

Ссылочные типы в основном делятся на четыре типа: сильные, слабые и слабые:

  1. Строгая ссылка относится к вездесущему методу присваивания в коде, например A a = new A(). Объекты, связанные с сильными ссылками, никогда не будут возвращены сборщиком мусора.
  2. Мягкие ссылки можно описать как SoftReferences, ссылаясь на полезные, но не необходимые объекты. Система восстановит такие объекты, на которые ссылаются, до того, как произойдет переполнение памяти.
  3. Слабые ссылки можно описать с помощью WeakReference, который по силе ниже, чем мягкие ссылки.Объекты со слабыми ссылками обязательно будут утилизированы при следующем сборщике мусора, независимо от того, достаточно ли памяти.
  4. Фантомные ссылки, также известные как фантомные ссылки, представляют собой самую слабую ссылочную связь и могут быть описаны PhantomReference. Они должны использоваться вместе с ReferenceQueue. Аналогично, когда происходит GC, виртуальные ссылки также будут переработаны. Памятью вне кучи можно управлять с помощью фантомных ссылок.

Вы знаете принцип пула потоков?

Прежде всего, пул потоков имеет несколько основных концепций параметров:

  1. Максимальное количество потоков maxPoolSize

  2. Количество основных потоков corePoolSize

  3. активное время

  4. Блокировка очереди workQueue

  5. Политика отклонения

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

  1. Когда мы отправляем задачу, пул потоков создаст количество задач в соответствии с размером corePoolSize.Поток выполняет задачу
  2. Когда количество задач превышает количество corePoolSize, последующие задачи попадут в очередь блокировки и заблокируют очередь.
  3. Когда блокирующая очередь также заполнена, она продолжит создавать потоки (maximumPoolSize-corePoolSize) для выполнения задачи.Если обработка задачи завершена, дополнительные потоки, созданные maximumPoolSize-corePoolSize, будут автоматически уничтожены после ожидания keepAliveTime.
  4. Если максимальный размер пула достигнут, а очередь блокировки все еще заполнена, она будет обработана в соответствии с другими политиками отклонения.

Какова политика отказа?

Существует 4 основных стратегии отказа:

  1. AbortPolicy: отменить задачу напрямую и создать исключение, это политика по умолчанию.
  2. CallerRunsPolicy: использовать поток вызывающего абонента только для обработки задач.
  3. DiscardOldestPolicy: отменить последнюю задачу в очереди ожидания и выполнить текущую задачу.
  4. DiscardPolicy: отбрасывать задачи напрямую, не вызывая исключений.

Кроме того, открыта читательская группа, честно говоря, даже если вы не говорите, в группе можно многому научиться. добавь меня в чат, примечаниев группу, я тяну тебя!