Вопросы для собеседования по многопоточности Java
Процесс — это автономная работающая среда, которую можно рассматривать как программу или приложение. Поток — это задача, выполняемая в процессе. Среда выполнения Java — это единый процесс, содержащий различные классы и программы. Потоки можно назвать легковесными процессами. Потокам требуется меньше ресурсов для создания и размещения в процессе, и они могут совместно использовать ресурсы внутри процесса.
В многопоточной программе несколько потоков выполняются одновременно для повышения эффективности программы. ЦП не переходит в состояние ожидания, поскольку потоку необходимо ждать ресурсов. Несколько потоков совместно используют кучную память, поэтому лучше создать несколько потоков для выполнения некоторых задач, чем создавать несколько процессов. Например, сервлеты лучше, чем CGI, потому что сервлеты поддерживают многопоточность, а CGI — нет.
Когда мы создаем поток в программе Java, он называется пользовательским потоком. Поток демона — это поток, который выполняется в фоновом режиме и не препятствует завершению работы JVM. Когда ни один из пользовательских потоков не запущен, JVM закрывает программу и завершает работу. Дочерние потоки, созданные потоком демона, по-прежнему остаются потоками демона.
Существует два способа создания потока: один — реализовать интерфейс Runnable и затем передать его конструктору Thread для создания объекта Thread, другой — напрямую наследовать класс Thread; Если вы хотите узнать больше, вы можете прочитать эту статью о том, как создавать потоки в Java.
Когда мы создаем новый поток в программе Java, его статус — «Новый». Когда мы вызываем метод start() потока, его статус меняется на Runnable. Планировщик потоков выделяет время ЦП потокам в пуле Runnable потоков и меняет их статус на «Выполняется». Другие состояния потока включают «Ожидание», «Заблокировано» и «Неактивно». Прочтите эту статью, чтобы узнать больше о жизненном цикле потока.
Конечно, но если мы вызовем метод run() Thread, он будет вести себя как обычный метод. Чтобы выполнить наш код в новом потоке, мы должны использовать метод Thread.start().
Мы можем использовать метод Sleep() класса Thread, чтобы приостановить поток на определенный период времени. Следует отметить, что это не завершает поток. Как только поток выйдет из спящего режима, его статус изменится на «Выполняемый», и он будет выполняться в соответствии с расписанием потока.
Каждый поток имеет приоритет. Вообще говоря, потоки с высоким приоритетом будут иметь приоритет при запуске, но это зависит от реализации планирования потоков, которое зависит от ОС. Мы можем определить приоритет потоков, но это не гарантирует, что потоки с высоким приоритетом будут выполняться раньше потоков с низким приоритетом. Приоритет потока — это переменная типа int (от 1 до 10), 1 представляет самый низкий приоритет, 10 — самый высокий приоритет.
Планировщик потоков — это служба операционной системы, которая отвечает за распределение процессорного времени для потоков в состоянии «Выполняемый». Как только мы создаем поток и запускаем его, его выполнение зависит от реализации планировщика потоков. Квантование времени относится к процессу распределения доступного процессорного времени доступным исполняемым потокам. Распределение времени ЦП может основываться на приоритете потока или времени ожидания потока. Планирование потоков не контролируется виртуальной машиной Java, поэтому лучше, чтобы приложение управляло им (то есть не делайте вашу программу зависимой от приоритета потока).
Переключение контекста — это процесс сохранения и восстановления состояния процессора, который позволяет возобновить выполнение потока с точки прерывания. Переключение контекста является важной особенностью многозадачных операционных систем и многопоточных сред.
Мы можем использовать метод Joint() класса Thread, чтобы гарантировать, что все потоки, созданные программой, завершатся до завершения работы метода main(). Вот статья о методе Joint() класса Thread.
Когда ресурсы могут быть разделены между потоками, взаимодействие между потоками является важным средством их координации. Методы wait()/notify()/notifyAll() в классе Object могут использоваться для обмена информацией между потоками о состоянии блокировок ресурсов. Нажмите здесь, чтобы узнать больше об ожидании потока, уведомлении и notifyAll.
Каждый объект в Java имеет блокировку (монитор, который также может быть монитором), а такие методы, как wait() и notify(), используются для ожидания блокировки объекта или уведомления других потоков о доступности монитора объекта. Ни для одного объекта в потоках Java нет блокировок или синхронизаторов. Вот почему эти методы являются частью класса Object, поэтому каждый класс в Java имеет базовые методы для межпоточного взаимодействия.
Когда потоку необходимо вызвать метод wait() объекта, он должен владеть блокировкой объекта. Затем он снимает блокировку объекта и переходит в состояние ожидания, пока другие потоки не вызовут метод notify() для объекта. Аналогично, когда потоку необходимо вызвать метод notify() объекта, он снимает блокировку объекта, чтобы другие ожидающие потоки могли получить блокировку объекта. Поскольку все эти методы требуют, чтобы поток удерживал блокировку объекта, что может быть достигнуто только посредством синхронизации, их можно вызывать только в синхронизированных методах или синхронизированных блоках.
Методы Sleep() и Give() класса Thread будут выполняться в текущем выполняющемся потоке. Поэтому нет смысла вызывать эти методы в других ожидающих потоках. Вот почему эти методы являются статическими. Они могут работать в текущем исполняющемся потоке и не позволять программистам ошибочно думать, что эти методы можно вызывать в других невыполняющихся потоках.
Существует множество способов обеспечить потокобезопасность в Java — синхронизация, использование атомарных параллельных классов, реализация параллельных блокировок, использование ключевого слова Volatible, использование неизменяемых классов и поточно-ориентированных классов. Вы можете узнать больше в руководстве по потокобезопасности.
Когда мы используем ключевое слово voluty для изменения переменной, поток будет читать переменную напрямую, а не кэшировать ее. Это гарантирует, что переменные, читаемые потоком, совпадают с переменными в памяти.
Синхронизированный блок — лучший выбор, поскольку он не блокирует весь объект (конечно, вы также можете сделать так, чтобы он блокировал весь объект). Синхронизированные методы блокируют весь объект, даже если в классе имеется несколько несвязанных синхронизированных блоков, что обычно приводит к остановке их выполнения и необходимости ждать, чтобы получить блокировку объекта.
Поток можно установить как поток демона с помощью метода setDaemon(true) класса Thread. Следует отметить, что этот метод необходимо вызывать перед вызовом метода start(), в противном случае будет выдано исключение IllegalThreadStateException.
ThreadLocal используется для создания локальных переменных потока. Мы знаем, что все потоки объекта будут использовать общие переменные, поэтому эти переменные не являются потокобезопасными. Мы можем использовать технологию синхронизации. Но если мы не хотим использовать синхронизацию, мы можем выбрать переменные ThreadLocal.
Каждый поток будет иметь свои собственные переменные Thread, и они смогут использовать методы get()/set() для получения значений по умолчанию или изменения своих значений внутри потока. Экземпляры ThreadLocal обычно хотят, чтобы связанное с ними состояние потока было частными статическими свойствами. В статье с примером ThreadLocal вы можете увидеть небольшую программу о ThreadLocal.
ThreadGroup — это класс, целью которого является предоставление информации о группах потоков.
API ThreadGroup относительно слаб и не предоставляет больше функций, чем Thread. Он имеет две основные функции: одна — получить список активных потоков в группе потоков; другая — установить для потока обработчик неперехваченных исключений (ncaught handler). Однако в Java 1.5 в класс Thread также добавлен метод setUncaughtExceptionHandler(UncaughtExceptionHandler eh), поэтому ThreadGroup устарел и его не рекомендуется использовать в дальнейшем.
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){ @Overridepublic void uncaughtException(Thread t, Throwable e) {System.out.println("произошло исключение:"+e.getMessage());} });
Дамп потока — это список активных потоков JVM, который очень полезен для анализа узких мест и взаимоблокировок системы. Есть много способов получить дампы потоков — с помощью Profiler, команды Kill -3, инструмента jstack и т. д. Я предпочитаю инструмент jstack, потому что он прост в использовании и поставляется с JDK. Поскольку это инструмент на базе терминала, мы можем написать несколько сценариев для периодического создания дампов потоков для анализа. Прочтите этот документ, чтобы узнать больше о создании дампов потоков.
Взаимная блокировка относится к ситуации, когда более двух потоков блокируются навсегда. Для этой ситуации требуется как минимум еще два потока и более двух ресурсов.
Чтобы проанализировать взаимоблокировку, нам нужно просмотреть дамп потока Java-приложения. Нам нужно выяснить, какие потоки находятся в статусе BLOCKED и какие ресурсы они ждут. Каждый ресурс имеет уникальный идентификатор, используя этот идентификатор, мы можем узнать, какие потоки уже владеют его блокировкой объекта.
Избегайте вложенных блокировок, используйте блокировки только там, где это необходимо, и избегайте неопределенного ожидания — это распространенные способы избежать взаимоблокировок. Прочтите эту статью, чтобы узнать, как анализировать взаимоблокировки.
java.util.Timer — это класс инструмента, который можно использовать для планирования выполнения потока в определенное время в будущем. Класс Timer можно использовать для планирования одноразовых или периодических задач.
java.util.TimerTask — это абстрактный класс, реализующий интерфейс Runnable. Нам нужно наследовать этот класс, чтобы создавать собственные запланированные задачи и использовать Timer для планирования их выполнения.
Вот примеры Java-таймера.
Пул потоков управляет группой рабочих потоков, а также включает очередь для размещения задач, ожидающих выполнения.
java.util.concurrent.Executors предоставляет реализацию интерфейса java.util.concurrent.Executor для создания пулов потоков. В примере пула потоков показано, как создать и использовать пул потоков, или прочитайте пример ScheduledThreadPoolExecutor, чтобы узнать, как создавать периодическую задачу.
Вопросы для собеседования по Java concurrency
Атомарная операция относится к блоку задачи операции, на который не влияют другие операции. Атомарные операции являются необходимым средством предотвращения несогласованности данных в многопоточной среде.
int++ не является атомарной операцией, поэтому, когда один поток считывает свое значение и добавляет 1, другой поток может прочитать предыдущее значение, что приведет к ошибке.
Чтобы решить эту проблему, мы должны гарантировать, что операция увеличения является атомарной. До JDK1.5 мы могли использовать для этого технологию синхронизации. Начиная с JDK 1.5, пакет java.util.concurrent.atomic предоставляет классы загрузки типов int и long, которые автоматически гарантируют, что их операции являются атомарными и не требуют использования синхронизации. Вы можете прочитать эту статью, чтобы узнать об атомарных классах Java.
Интерфейс Lock обеспечивает более масштабируемые операции блокировки, чем синхронизированные методы и синхронизированные блоки. Они позволяют создавать более гибкие структуры, которые могут иметь совершенно разные свойства и поддерживать несколько связанных классов условных объектов.
Его преимущества:
Подробнее о примерах блокировок
Платформа Executor была представлена в Java 5 с интерфейсом java.util.concurrent.Executor. Платформа Executor — это платформа для асинхронных задач, которые вызываются, планируются, выполняются и контролируются в соответствии с набором стратегий выполнения.
Неограниченное создание потоков может привести к переполнению памяти приложения. Поэтому создание пула потоков — лучшее решение, поскольку количество потоков можно ограничить, а эти потоки можно перерабатывать и использовать повторно. Очень удобно создавать пул потоков с помощью платформы Executors. Прочтите эту статью, чтобы узнать, как создать пул потоков с помощью платформы Executor.
Характеристики java.util.concurrent.BlockingQueue таковы: когда очередь пуста, операция получения или удаления элементов из очереди будет заблокирована, или когда очередь заполнена, операция добавления элементов в очередь будет заблокирована. .
Блокирующая очередь не принимает нулевые значения. При попытке добавить в очередь нулевое значение будет выдано исключение NullPointerException.
Реализации блокирующей очереди являются потокобезопасными, а все методы запросов являются атомарными и используют внутренние блокировки или другие формы управления параллелизмом.
Интерфейс BlockingQueue является частью платформы коллекций Java и в основном используется для реализации проблемы производитель-потребитель.
Прочтите эту статью, чтобы узнать, как реализовать проблему производитель-потребитель с помощью блокирующих очередей.
В Java 5 в пакете параллелизма появился интерфейс java.util.concurrent.Callable, который очень похож на интерфейс Runnable, но может возвращать объект или генерировать исключение.
Интерфейс Callable использует дженерики для определения типа возвращаемого значения. Класс Executors предоставляет несколько полезных методов для выполнения задач внутри Callable в пуле потоков. Поскольку задача Callable является параллельной, нам приходится ждать возвращаемого ею результата. Объект java.util.concurrent.Future решает за нас эту проблему. После того, как пул потоков отправляет вызываемую задачу, возвращается объект Future. Используя его, мы можем узнать состояние вызываемой задачи и получить результат выполнения, возвращаемый вызываемой задачей. Future предоставляет метод get(), чтобы мы могли дождаться завершения Callable и получить результаты его выполнения.
Прочтите эту статью, чтобы узнать больше примеров о Callable и Future.
FutureTask — это базовая реализация Future, которую мы можем использовать с исполнителями для обработки асинхронных задач. Обычно нам не нужно использовать класс FutureTask, но он становится очень полезным, когда мы планируем переопределить некоторые методы интерфейса Future и сохранить исходную базовую реализацию. Мы можем просто наследовать от него и переопределить нужные нам методы. Прочтите пример Java FutureTask, чтобы узнать, как его использовать.
Классы коллекций Java являются отказоустойчивыми, а это означает, что когда коллекция изменяется и поток использует итератор для обхода коллекции, метод итератора next() выдаст исключение ConcurrentModificationException.
Параллельные контейнеры поддерживают одновременный обход и одновременные обновления.
Основными классами являются ConcurrentHashMap, CopyOnWriteArrayList и CopyOnWriteArraySet. Прочтите эту статью, чтобы узнать, как избежать ConcurrentModificationException.
Исполнители предоставляют некоторые служебные методы для классов Executor, ExecutorService, ScheduledExecutorService, ThreadFactory и Callable.
Исполнители можно использовать для легкого создания пулов потоков.
Оригинальный текст: Journaldev.com Перевод: ifeve Переводчик: Чжэн Сюдун