Рубрики
Без рубрики

Вопросы для интервью с Параллелизмом Java (+ Ответы)

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

Автор оригинала: baeldung.

1. введение

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

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

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

С точки зрения операционной системы, процесс-это независимая часть программного обеспечения, которая работает в своем собственном пространстве виртуальной памяти. Любая многозадачная операционная система (а это означает, что почти любая современная операционная система) должна разделять процессы в памяти, чтобы один неудачный процесс не тянул все другие процессы вниз, скремблируя общую память.

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

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

Q2. Как Вы можете создать экземпляр потока и запустить его?

Чтобы создать экземпляр потока, у вас есть два варианта. Сначала передайте Выполняемый экземпляр его конструктору и вызовите start() . Запускаемый – это функциональный интерфейс, поэтому он может быть передан в виде лямбда-выражения:

Thread thread1 = new Thread(() ->
  System.out.println("Hello World from Runnable!"));
thread1.start();

Поток также реализует Запускаемый , поэтому другим способом запуска потока является создание анонимного подкласса, переопределение его метода run () , а затем вызов start() :

Thread thread2 = new Thread() {
    @Override
    public void run() {
        System.out.println("Hello World from subclass!");
    }
};
thread2.start();

Q3. Опишите Различные состояния потока и Когда происходят переходы состояний.

Состояние Поток можно проверить с помощью метода Thread.getState () . Различные состояния a Поток описан в Потоке.Состояние перечисление. Они являются:

  • НОВЫЙ — новый Поток экземпляр, который еще не был запущен с помощью Thread.start()
  • ЗАПУСКАЕМЫЙ — запущенный поток. Он называется выполняемым, потому что в любой момент времени он может быть либо запущен, либо ожидать следующего кванта времени от планировщика потоков. НОВЫЙ поток переходит в РАБОТОСПОСОБНОЕ состояние при вызове Thread.start() на нем
  • ЗАБЛОКИРОВАН — запущенный поток блокируется, если ему необходимо войти в синхронизированный раздел, но он не может этого сделать из-за того, что другой поток держит монитор этого раздела
  • ОЖИДАНИЕ — поток переходит в это состояние, если он ожидает, пока другой поток выполнит определенное действие. Например, поток переходит в это состояние при вызове метода Object.wait() на мониторе, который он содержит, или метода Thread.join() в другом потоке
  • TIMED_WAITING — то же, что и выше, но поток переходит в это состояние после вызова синхронизированных версий Thread.sleep () , Object.wait () , Thread.join() и некоторых других методов
  • ЗАВЕРШЕНО — поток завершил выполнение своего метода Runnable.run() и завершен

Q4. В чем разница между Управляемым и Вызываемым интерфейсами? Как Они Используются?

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

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

Q5. Что Такое Поток Демона, Каковы Его Варианты Использования? Как Вы можете Создать Поток Демона?

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

Чтобы запустить поток как демон, вы должны использовать метод setDaemon() перед вызовом start() :

Thread daemon = new Thread(()
  -> System.out.println("Hello from daemon!"));
daemon.setDaemon(true);
daemon.start();

Любопытно, что если вы запустите это как часть метода main () , сообщение может не быть напечатано. Это может произойти, если поток main() завершится до того, как демон доберется до точки печати сообщения. Как правило, вам не следует выполнять какие-либо операции ввода-вывода в потоках демонов, так как они даже не смогут выполнить свои/|, наконец, блоки и закрыть ресурсы, если они будут оставлены.

Q6. Каков Флаг прерывания потока? Как Вы можете Установить и проверить Это? Как это связано с исключением Interruptedexception?

Флаг прерывания или статус прерывания-это внутренний Поток флаг, который устанавливается при прерывании потока. Чтобы установить его, просто вызовите thread.interrupt() для объекта thread .

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

Если поток не находится внутри такого метода и вызывается thread.interrupt () , ничего особенного не происходит. Ответственность потока заключается в периодической проверке состояния прерывания с помощью метода static Thread.interrupted() или экземпляра isInterrupted () . Разница между этими методами заключается в том, что статический поток.прерванный() удаляет флаг прерывания, в то время как Прерывается() не делает этого.

Q7. Что такое Исполнитель и Служба исполнителей? В Чем Различия Между Этими Интерфейсами?

Исполнитель и ExecutorService являются двумя связанными интерфейсами java.util.concurrent framework. Исполнитель – это очень простой интерфейс с одним исполняемым методом, принимающим Запускаемые экземпляры для выполнения. В большинстве случаев это интерфейс, от которого должен зависеть ваш код, выполняющий задачу.

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

Для получения дополнительной информации об использовании Executor и ExecutorService см. статью Руководство по Java ExecutorService .

Q8. Каковы доступные реализации Executorservice в стандартной библиотеке?

Интерфейс ExecutorService имеет три стандартные реализации:

  • ThreadPoolExecutor — для выполнения задач с использованием пула потоков. Как только поток завершает выполнение задачи, он возвращается в пул. Если все потоки в пуле заняты, то задача должна дождаться своей очереди.
  • ScheduledThreadPoolExecutor позволяет планировать выполнение задачи вместо ее немедленного запуска при наличии потока. Он также может планировать задачи с фиксированной скоростью или фиксированной задержкой.
  • ForkJoinPool – это специальный ExecutorService для решения задач рекурсивных алгоритмов. Если вы используете обычный ThreadPoolExecutor для рекурсивного алгоритма, вы быстро обнаружите, что все ваши потоки заняты ожиданием завершения более низких уровней рекурсии. ForkJoinPool реализует так называемый алгоритм кражи работы, который позволяет ему более эффективно использовать доступные потоки.

Q9. Что Такое Модель Памяти Java (Jmm)? Опишите Его Назначение и основные идеи.

Модель памяти Java является частью спецификации языка Java, описанной в Глава 17.4 . Он определяет, как несколько потоков получают доступ к общей памяти в параллельном приложении Java, и как изменения данных одним потоком становятся видимыми для других потоков. Будучи довольно коротким и лаконичным, JMM может быть трудно понять без сильной математической подготовки.

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

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

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

Основными понятиями JMM являются:

  • Действия , это действия между потоками, которые могут выполняться одним потоком и обнаруживаться другим потоком, например чтение или запись переменных, блокировка/разблокировка мониторов и так далее
  • Действия синхронизации , определенное подмножество действий, таких как чтение/запись изменчивой переменной или блокировка/разблокировка монитора
  • Порядок программы (PO), наблюдаемый общий порядок действий внутри одного потока
  • Порядок синхронизации (SO), общий порядок между всеми действиями синхронизации — он должен соответствовать Порядку программы, то есть, если два действия синхронизации происходят одно перед другим в PO, они происходят в одном и том же порядке в SO
  • синхронизирует-с (SW) отношением между определенными действиями синхронизации, такими как разблокировка монитора и блокировка одного и того же монитора (в другом или том же потоке)
  • Происходит-перед порядком — объединяет PO с SW (это называется транзитивным замыканием в теории множеств) для создания частичного упорядочения всех действий между потоками. Если одно действие происходит-до другого, то результаты первого действия наблюдаются вторым действием (например, запись переменной в одном потоке и чтение в другом).
  • Происходит до согласованности -набор действий согласован с HB, если при каждом чтении наблюдается либо последняя запись в это место в порядке “Происходит до”, либо какая — либо другая запись через гонку данных
  • Выполнение — определенный набор упорядоченных действий и правил согласованности между ними

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

Q10. Что такое Изменчивое Поле и Какие гарантии Дает Jmm для Такого Поля?

Поле volatile обладает специальными свойствами в соответствии с моделью памяти Java (см. Q9). Операции чтения и записи переменной volatile являются действиями синхронизации, что означает, что они имеют общий порядок (все потоки будут соблюдать согласованный порядок этих действий). При чтении изменчивой переменной гарантируется, что последняя запись в эту переменную будет выполнена в соответствии с этим порядком.

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

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

Q11. Какие из Следующих операций Являются Атомарными?

  • запись в не энергонезависимый |/int ; запись в
  • volatile int ; запись в не/| энергонезависимый длинный
  • ; запись в изменчивый длинный
  • ; увеличение изменчивой длины
  • ?

Запись в int (32-разрядную) переменную гарантированно будет атомной, независимо от того, является ли она изменчивой или нет. Переменная long (64-разрядная) может быть записана в два отдельных шага, например, на 32-разрядных архитектурах, поэтому по умолчанию гарантия атомарности отсутствует. Однако, если вы укажете модификатор volatile , доступ к переменной long будет гарантирован атомарным способом.

Операция приращения обычно выполняется в несколько этапов (получение значения, его изменение и обратная запись), поэтому никогда не гарантируется, что оно будет атомарным, независимо от того, является ли переменная изменчивой или нет. Если вам нужно реализовать атомарное приращение значения, вы должны использовать классы AtomicInteger , AtomicLong и т.д.

Q12. Какие специальные гарантии дает Jmm для Конечных полей класса?

JVM в основном гарантирует, что окончательные поля класса будут инициализированы до того, как какой-либо поток завладеет объектом. Без этой гарантии ссылка на объект может быть опубликована, т. Е. стать видимой, в другом потоке до того, как все поля этого объекта будут инициализированы из-за изменения порядка или других оптимизаций. Это может привести к частому доступу к этим полям.

Вот почему при создании неизменяемого объекта вы всегда должны делать все его поля окончательными , даже если они недоступны с помощью методов getter.

Q13. Что означает ключевое слово Synchronized в определении метода? статического метода? Перед Кварталом?

Ключевое слово synchronized перед блоком означает, что любой поток, входящий в этот блок, должен получить монитор (объект в скобках). Если монитор уже получен другим потоком, первый поток перейдет в состояние ЗАБЛОКИРОВАН и будет ждать, пока монитор не будет освобожден.

synchronized(object) {
    // ...
}

Метод synchronized экземпляра имеет ту же семантику, но сам экземпляр действует как монитор.

synchronized void instanceMethod() {
    // ...
}

Для статического синхронизированного метода монитором является Класс объект, представляющий объявляющий класс.

static synchronized void staticMethod() {
    // ...
}

Q14. Если два потока одновременно Вызывают Синхронизированный метод на разных Экземплярах Объектов, Может ли Один из Этих Потоков Заблокировать? Что Делать, Если метод Статичен?

Если метод является методом экземпляра, то экземпляр действует как монитор для метода. Два потока, вызывающие метод в разных экземплярах, получают разные мониторы, поэтому ни один из них не блокируется.

Если метод статический , то монитор является классом объектом. Для обоих потоков монитор одинаков, поэтому один из них, вероятно, заблокируется и будет ждать, пока другой выйдет из метода synchronized .

Q15. Какова цель методов Wait, Notify и Notifyall класса объектов?

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

Когда другой поток, получивший монитор, выполняет условие, он может вызвать object.notify() или object.notifyAll() и освободить монитор. Метод notify выводит один поток в состояние ожидания, а метод notifyAll выводит все потоки, ожидающие этого монитора, и все они соревнуются за повторное получение блокировки.

Следующее Реализация BlockingQueue показывает, как несколько потоков работают вместе с помощью шаблона wait-notify . Если мы поместим элемент в пустую очередь, все потоки, ожидавшие в методе take , проснутся и попытаются получить значение. Если мы помещаем элемент в полную очередь, помещаем метод ждем s для вызова метода get . Метод get удаляет элемент и уведомляет потоки, ожидающие в методе put , что в очереди есть пустое место для нового элемента.

public class BlockingQueue {

    private List queue = new LinkedList();

    private int limit = 10;

    public synchronized void put(T item) {
        while (queue.size() == limit) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        if (queue.isEmpty()) {
            notifyAll();
        }
        queue.add(item);
    }

    public synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        if (queue.size() == limit) {
            notifyAll();
        }
        return queue.remove(0);
    }
    
}

Q16. Опишите Условия Тупика, Остановки и Голода. Опишите возможные Причины этих состояний.

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

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

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

Q17. Опишите назначение и варианты использования платформы Fork/Join.

Платформа fork/join позволяет распараллеливать рекурсивные алгоритмы. Основная проблема с распараллеливанием рекурсии с использованием чего-то вроде ThreadPoolExecutor заключается в том, что у вас может быстро закончиться поток, потому что для каждого рекурсивного шага потребуется свой собственный поток, в то время как потоки в стеке будут простаивать и ждать.

Точкой входа в структуру fork/join является класс ForkJoinPool , который является реализацией ExecutorService . Он реализует алгоритм кражи работы, при котором незанятые потоки пытаются “украсть” работу у занятых потоков. Это позволяет распределять вычисления между различными потоками и добиваться прогресса при использовании меньшего количества потоков, чем это потребовалось бы при использовании обычного пула потоков.

Более подробную информацию и примеры кода для платформы fork/join можно найти в статье “Руководство по платформе Fork/Join в Java” .