Сценарии использования многопоточности JAVA и меры предосторожности

Java

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

Он спросил меня: что такое ConcurrentHashMap? ---

Программирование — это не шоу. В большинстве случаев, как написать простой код, это умение.

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

нить

Thread

Первая категорияThreadсвоего рода. Мы все знаем, что есть две реализации. Первый может передаваться по наследству.Threadнакрой этоrunметод; второй заключается в реализацииRunnableинтерфейс, который его реализуетrunметод; и третий способ создать поток — через пул потоков.

Наша конкретная реализация кода находится в методе run.

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

выход потока

После выполнения некоторых методов запуска поток завершится. Но некоторые методы запуска никогда не заканчиваются. Окончание темы определенно не черезThread.stop()этот метод устарел в версии java1.2. Таким образом, у нас обычно есть два способа управления потоками.

Определите флаг выхода и поместите его в то время как

Код обычно выглядит так.

private volatile boolean flag= true;
public void run() {
    while (flag) {
    }
}

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

Завершить поток, используя метод прерывания

Подобно этому.

while(!isInterrupted()){……}

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

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

При использовании серии прерываний вы должны быть внимательны, это приведет к ошибкам и даже взаимоблокировкам.

Обработка исключений

В java выбрасываются два исключения. Одно должно быть перехвачено, например InterruptedException, иначе оно не может быть скомпилировано, а другое может быть обработано или нет, например NullPointerException.

При выполнении нашей задачи очень вероятно, что будут выброшены эти два типа исключений. Для первого исключения его нужно поместить в try and catch. Однако если второе исключение не будет обработано, это повлияет на нормальную работу задачи.

Многие студенты не улавливали некоторые неявные исключения при работе с циклическими задачами, что приводило к невозможности продолжения выполнения задачи при возникновении исключения. Если вы не можете определить тип исключения, вы можете напрямую поймать Exception или более общий Throwable.

while(!isInterrupted()){
    try{
        ……
    }catch(Exception ex){
        ……
    }
}

Синхронно

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

  • synchronizedключевые слова
  • ждать, уведомлять и т. д.
  • в параллельном пакетеReentrantLock
  • volatileключевые слова
  • Локальные переменные ThreadLocal

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

Наиболее подверженной ошибкам при использовании ReentrantLock является забывание закрыть блокировку в блоке finally. В большинстве сценариев синхронизации достаточно использовать блокировку, а также имеется концепция блокировки чтения-записи для детального контроля. Обычно мы используем несправедливые блокировки, чтобы позволить задачам свободно конкурировать. Производительность недобросовестных блокировок выше, чем у честных блокировок.Недобросовестные блокировки могут более полно использовать квант времени ЦП и минимизировать время простоя ЦП. Несправедливые блокировки также могут привести к голоданию: некоторые задачи никогда не получают блокировки.

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

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

И синхронизированные, и изменчивые отражаются в байт-коде (monitorenter, monitorexit), в основном добавляя барьеры памяти. И Lock - это чистый Java API.

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

О параллельном пакете

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

Пул потоков

Наиболее полный пул потоков имеет около 7 параметров.Если вы хотите разумно использовать пул потоков, вы точно не пропустите оптимизацию этих параметров.

параметры пула потоков

Наиболее часто используемый параллельный пакет — это пул потоков, рекомендуется использовать пул потоков напрямую для обычной работы, а классу Thread можно понизить приоритет. В основном мы используем newSingleThreadExecutor, newFixedThreadPool, newCachedThreadPool, планирование и т. д., которые создаются с помощью фабричного класса Executors.

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

Я обычно настраиваю ThreadPoolExecutor, который имеет наиболее полные параметры.

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) 

Если мою задачу можно оценить, то для corePoolSize и maxPoolSize обычно устанавливается один и тот же размер, а затем устанавливается особенно длительное время выживания. Можно избежать накладных расходов на частое создание и закрытие потоков. Потоки приложений с интенсивным вводом-выводом и с интенсивным использованием ЦП имеют разные размеры.Как правило, потоки приложений с интенсивным вводом-выводом могут открывать больше потоков.

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

монитор

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

Обычно вы можете контролировать и отслеживать поведение потока, наследуя ThreadPoolExecutor и переопределяя методы beforeExecute, afterExecute, Terminated.

Политика насыщения пула потоков

Вероятно, самым забытым из них является политика насыщения потоков. То есть, когда пространство потоков и буферных очередей будет полностью израсходовано, как будут избавляться от вновь добавленных задач. JDK по умолчанию реализует 4 стратегии, и реализация по умолчаниюAbortPolicy, то есть генерировать исключение напрямую. Другие описаны ниже.

DiscardPolicyЭто более агрессивно, чем прерывание, оно просто отбрасывает задачу и даже не содержит какой-либо ненормальной информации.

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

DiscardOldestPolicyОтмените задачу в верхней части очереди и повторите задачу (повторите процесс).

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

очередь блокировки

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

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

Максимальная длина очереди LinkedBlockingQueue по умолчанию — Integer.MAX_VALUE, что опасно при использовании в качестве очереди пула потоков.

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

DelayQueue — это неограниченная блокирующая очередь, поддерживающая отложенное получение элементов. Объект, помещенный в DelayQueue, должен реализовать интерфейс Delayed, в основном для предоставления времени задержки, а также для внутреннего сравнения и сортировки очереди задержки. Такой подход обычно экономит больше ресурсов процессора, чем большинство неблокирующих циклов while.

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

CountDownLatch

Если есть куча интерфейсов A-Y, максимальное время работы каждого интерфейса составляет 200 мс, а минимальное — 100 мс.

Один из моих сервисов должен предоставить интерфейс Z и вызвать интерфейс A-Y для агрегирования результатов. Для интерфейсных вызовов не требуется последовательности.Как интерфейс Z возвращает эти данные в течение 300 мс?

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

  • Реализовать параллелизм задач
  • Подождите, пока n потоков завершат задачи, прежде чем начать выполнение

Перед одновременным появлением пакета подготовка этого требует ручной процесс синхронизации очень сложно. Теперь вы можете использовать CountDownLatch и CyclicBarrier для удобного кодирования.

CountDownLatch реализуется счетчиком, начальным значением которого является количество потоков. Каждый раз, когда поток завершает свою задачу, значение счетчика уменьшается на 1. Когда значение счетчика достигает 0, это указывает на то, что все потоки завершили задачу, и тогда поток, ожидающий защелки, может возобновить выполнение задачи. CyclicBarrier аналогичен и может выполнять ту же функцию. Однако в повседневной работе CountDownLatch будет использоваться чаще.

сигнал

Хотя у Semaphore есть несколько сценариев применения, большинство из них — великолепные навыки, и их следует использовать как можно реже при кодировании.

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

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

Lock && Condition

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

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

End

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

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

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