Мы использовали пул потоков или меньше в работе, но почему я должен использовать пул потоков? Из его имени мы должны знать, что пул потоков использует технологию пула, как и многие другие методы пулирования, все для более эффективного использования ресурсов, таких как пулы ссылок, пулы памяти и многое другое.
Ссылка на базу данных является очень дорогим ресурсом, и за создание и уничтожение нужно платить высокую цену.Чтобы избежать частого создания ссылок на базу данных, производится технология пула ссылок. Сначала создайте пакет ссылок базы данных в пуле.Когда вам нужно получить доступ к базе данных, перейдите непосредственно в пул, чтобы получить доступную ссылку, а затем верните ее в пул ссылок после использования.
Точно так же потоки являются ценным ресурсом, но также и ограниченным ресурсом, а создание и уничтожение потоков одинаково затратно. Весь наш код поддерживается потоками один за другим, и сегодняшняя архитектура чипа также определяет, что мы должны писать программы, которые выполняют многопоточное выполнение, чтобы получить максимальную производительность программы.
Таким образом, как эффективно управлять разделением труда и взаимодействием между несколькими потоками стало ключевым вопросом.Год Дуг Ли разработал и реализовал для нас инструмент пула потоков.С помощью этого инструмента мы можем реализовать возможность многопоточности и реализовать задачу Эффективное исполнение и планирование.
Чтобы правильно и разумно использовать инструмент пула потоков, нам необходимо понять принцип работы пула потоков.
В этой статье пул потоков в основном анализируется с трех аспектов: состояние пула потоков, важные атрибуты и рабочий процесс.
состояние пула потоков
Во-первых, пул потоков имеет состояние.Эти состояния определяют некоторые условия работы внутри пула потоков.Процесс от открытия до закрытия пула потоков представляет собой процесс циркуляции состояния пула потоков.
Существует пять состояний пула потоков:
государство | значение |
---|---|
RUNNING | Состояние выполнения, в котором пул потоков может принимать новые задачи или обрабатывать задачи в очереди блокировки. Выполните метод выключения, чтобы войти в состояние ВЫКЛЮЧЕНИЯ. Выполните метод shutdownNow, чтобы войти в состояние STOP. |
SHUTDOWN | Чтобы быть закрытым, никакие новые задачи не будут приниматься, а задачи в очереди блокировки будут продолжать обрабатываться Когда задача в очереди блокировки пуста, а количество рабочих потоков равно 0, войдите в состояние TIDYING. |
STOP | Остановлено, не получает новых задач, не обрабатывает задачи в очереди блокировки и пытается завершить выполнение задач Когда количество рабочих потоков равно 0, войдите в состояние TIDYING. |
TIDYING | Завершение состояния, на данный момент задачи выполнены, а рабочих потоков нет. Войдите в состояние TERMINATED после выполнения завершенного метода |
TERMINATED | Завершенное состояние, в котором пул потоков полностью остановлен, а освобождение всех ресурсов завершено. |
важные атрибуты
Существует множество основных параметров пула потоков, каждый из которых имеет особую функцию.После того, как все параметры будут агрегированы вместе, будет завершена полная работа всего пула потоков.
1. Статус потока и количество рабочих потоков
Во-первых, пул потоков имеет состояние, поведение пула потоков в разных состояниях различно, пять состояний были упомянуты выше.
Кроме того, пул потоков определенно нуждается в потоках для выполнения определенных задач, поэтому внутренний класс Worker инкапсулируется в пуле потоков как рабочий поток, и каждый рабочий поток поддерживает поток.
Одним из ключевых моментов пула потоков является контроль за рациональным и эффективным использованием ресурсов потоков, поэтому необходимо контролировать количество рабочих потоков, поэтому необходимо сохранять количество рабочих потоков в текущем пуле потоков.
Видя это, думаете ли вы, что вам нужно использовать две переменные для сохранения состояния пула потоков и количества рабочих потоков в пуле потоков? Но в ThreadPoolExecutor для сохранения значений этих двух свойств используется только одна переменная типа AtomicInteger, то есть ctl.
Старшие 3 бита ctl используются для индикации состояния пула потоков (runState), а младшие 29 бит используются для указания количества рабочих потоков (workerCnt).Зачем использовать 3 бита для индикации состояния пула потоков Причина в том, что пул потоков имеет в общей сложности 5 состояний, а 2 бита могут представлять только 4 ситуации, поэтому для представления 5 состояний необходимо как минимум 3 бита.
2. Количество основных потоков и максимальное количество потоков
Теперь, когда есть переменная, которая отмечает количество рабочих потоков, сколько потоков должно быть? Слишком много потоков тратят впустую ресурсы потоков, а если потоков слишком мало, производительность пула потоков не может быть реализована.
Чтобы решить эту проблему, пул потоков разработал две переменные для взаимодействия, а именно:
Количество основных потоков: corePoolSize используется для представления количества основных потоков в пуле потоков, также известного как количество простаивающих потоков.
Максимальное количество потоков: maxPoolSize используется для указания максимального количества потоков, которые могут быть созданы в пуле потоков.
Теперь у нас вопрос: если уже есть переменные, определяющие количество рабочих потоков, то зачем нам количество основных потоков и максимальное количество потоков?
На самом деле, если подумать об этом таким образом, то можно понять, что создание потока имеет свою стоимость.Вы не можете создавать поток каждый раз, когда хотите выполнить задачу, но вы не можете, когда задач слишком много, только выполняется небольшое количество потоков, поэтому задача выполняется слишком поздно.Вместо этого он должен создать достаточное количество потоков для своевременной обработки задач. С изменением количества задач, когда количество задач заведомо очень мало, нет необходимости в сохранении первоначально созданных избыточных потоков, поскольку в это время для их обработки может использоваться небольшое количество потоков, поэтому количество реально работающих потоков, которое зависит от задачи.
Какова связь между количеством основных потоков, максимальным количеством потоков и количеством рабочих потоков?
Количество рабочих потоков может варьироваться от 0 до максимального количества потоков и может поддерживаться на уровне corePoolSize после периода выполнения, но не является абсолютным, в зависимости от того, разрешено ли освобождение основных потоков по тайм-ауту.
3. Фабрика, создающая поток
Поскольку это пул потоков, потоки, естественно, необходимы.Как создавать потоки? Эта задача передается для завершения фабрике потоков ThreadFactory.
4. Блокировка очереди задач кеша
Выше мы говорили о количестве основных потоков и максимальном количестве потоков, а также ввели, что количество рабочих потоков варьируется от 0 до максимального количества потоков. Но невозможно создать все потоки сразу и заполнить пул потоков, но есть процесс, который выглядит следующим образом:
Когда пул потоков получает задачу, если количество рабочих потоков не достигает corePoolSize, будет создан новый поток, и задача будет привязана, а предыдущий поток не будет повторно использоваться, пока количество рабочих потоков не достигнет corePoolSize.
Когда количество рабочих потоков достигает corePoolSize и получена новая задача, задача будет сохранена в очереди блокировки, чтобы дождаться выполнения основного потока. Почему бы просто не создать больше потоков для выполнения новых задач, причина в том, что в основном потоке есть вероятность, что некоторые потоки уже выполнили свои задачи, или есть другие потоки, которые могут закончить текущую задачу сразу, а затем могут инвестировать Перейти к новые задачи, поэтому блокирующая очередь представляет собой механизм буферизации, дающий основным потокам возможность полностью использовать свои возможности. Еще одна причина, которую стоит учитывать, заключается в том, что, в конце концов, создание потоков является относительно дорогим, и невозможно создать новый поток, как только есть задача для выполнения.
Поэтому нам нужно оборудовать пул потоков блокирующей очередью для временного кэширования задач, которые будут ожидать выполнения рабочих потоков.
5. Время выживания неосновного потока
Выше мы сказали, что когда количество рабочих потоков достигает corePoolSize, пул потоков будет хранить вновь полученные задачи в очереди блокировки, а очередь блокировки имеет две ситуации: одна — ограниченная очередь, а другая — неограниченная очередь.
Если это неограниченная очередь, то при занятости основных потоков все вновь отправленные задачи будут храниться в неограниченной очереди, в это время максимальное количество потоков станет бессмысленным, так как блокирующая очередь не будет заполнена. .
Если это ограниченная очередь, то когда блокирующая очередь заполнена задачами, ожидающими выполнения, и когда отправляется новая задача, пул потоков должен создать новый «временный» поток для ее обработки, что эквивалентно диспетчеризации. дополнительная рабочая сила для выполнения задачи.
Однако созданные «временные» потоки имеют время выживания, и поддерживать их все время в живых невозможно.Когда задачи в блокирующей очереди выполняются, а новых задач поступает не так много, «временным» потокам нужно чтобы быть Переработка и уничтожение, период времени ожидания перед переработкой и уничтожением является временем выживания неосновных потоков, которое является атрибутом keepAliveTime.
Так что же такое «неосновной поток»? Является ли поток, созданный первым, основным потоком, а поток, созданный позже, является неосновным потоком?
На самом деле основной поток не имеет ничего общего с порядком создания, а с количеством рабочих потоков.Если текущее количество рабочих потоков больше, чем количество основных потоков, то все потоки могут быть «неосновными потоками». ", все из которых были переработаны возможно.
После того, как поток завершает выполнение задачи, он извлекает новую задачу из очереди блокировки и находится в режиме ожидания до тех пор, пока задача не будет выбрана.
Есть два способа взять задачу: один — заблокировать ее до тех пор, пока задача не будет снята с помощью метода take(), а другой — снять задачу в течение определенного периода времени или тайм-аута с помощью опроса (keepAliveTime, timeUnit). Если истечет время ожидания, поток будет заблокирован. Перезапуск: обратите внимание, что основные потоки обычно не перерабатываются.
Так как же гарантировать, что основной поток не будет переработан? Это по-прежнему связано с количеством рабочих потоков.Когда каждый поток берет задачу, пул потоков сравнивает текущее количество рабочих потоков с количеством основных потоков:
Если количество рабочих потоков меньше текущего количества основных нитей, первый метод используется для привлечения задач, то есть нет ожидания для переработки. В это время все рабочие потоки являются «основными нитями», и они не будет переработан;
Если оно больше, чем количество основных потоков, для получения задачи используется второй метод, и после истечения времени ожидания он будет повторно использован, поэтому абсолютного основного потока не существует, пока поток не получает задачу. для выполнения в течение времени выживания, он будет переработан.
Следовательно, если каждый поток хочет сохранить свою идентичность «базового потока», он должен приложить все усилия для получения задач для выполнения как можно быстрее, чтобы избежать повторного использования.
Основные потоки, как правило, не перезапускаются, но это не является абсолютным. Если мы разрешим повторное использование основных потоков с течением времени, тогда не будет такой вещи, как основные потоки. Все потоки будут получать задачи через опрос (keepAliveTime, timeUnit). , как только задача не может быть получена с течением времени, она будет переработана.Как правило, она редко используется таким образом, если только пул потоков не должен обрабатывать очень мало задач и частота не высока, поэтому нет необходимости поддерживать ядро нить все время.
6. Политика отказа
Хотя у нас есть очередь блокировки для кэширования задач, которая в определенной степени обеспечивает буферный период для выполнения пула потоков, но если это ограниченная очередь блокировки, возникает ситуация, когда очередь заполнена, а также есть данные для рабочих потоков, когда достигнуто максимальное количество потоков. Если в это время отправляется новая задача, очевидно, что пула потоков имеет более чем достаточную емкость, потому что нет ни свободного места в очереди для хранения задачи, ни невозможно создать новый поток для выполнения задачи, поэтому в этот момент раз у нас должна быть стратегия отказа, то есть обработчик.
Политика отклонения — это переменная типа RejectedExecutionHandler. Пользователи могут указать политику отклонения самостоятельно. Если она не указана, пул потоков будет использовать политику отклонения по умолчанию: создать исключение.
В пуле резьбы также отказались предоставлять много других стратегий, которые могут быть выбраны для нас:
просто брось задачу
Выполнение задачи с помощью вызывающего потока
Отменить самую старую задачу в очереди задач, затем отправить задачу
процесс работы
После понимания всех важных свойств пула потоков теперь нам нужно понять рабочий процесс пула потоков.
Приведенное выше изображение представляет собой упрощенную картину работы пула потоков. Фактический процесс намного сложнее, чем этот, но они должны полностью охватывать весь рабочий процесс пула потоков.
Весь процесс можно разделить на следующие части:
1. Отправить задачу
При отправке новой задачи в пул потоков у пула потоков есть три условия обработки, а именно: создание рабочего потока для выполнения задачи, добавление задачи в очередь блокировки и отклонение задачи.
Процесс отправки заданий также можно разделить на следующие части:
Когда количество рабочих потоков меньше количества основных потоков, новый основной рабочий поток создается напрямую.
Когда количество рабочих потоков не меньше количества основных потоков, нужно попробовать добавить задачи в очередь блокировки
Если его можно успешно добавить, это означает, что очередь не заполнена, тогда необходимо выполнить следующую вторичную проверку, чтобы убедиться, что добавленные задачи могут быть успешно выполнены.
Проверьте состояние выполнения текущего пула потоков. Если он находится в состоянии НЕРАБОТАЕТ, вам необходимо удалить задачу из очереди блокировки, а затем отклонить задачу.
Проверьте количество рабочих потоков в текущем пуле потоков. Если оно равно 0, вам необходимо активно добавить пустой рабочий поток для выполнения задачи, только что добавленной в очередь блокировки.
Если соединение не удалось, очередь заполнена, то для выполнения задачи необходимо создать новый «временный» рабочий поток.
Если создание прошло успешно, выполнить задачу напрямую
Если создание не удается, это означает, что количество рабочих потоков равно максимальному количеству потоков, и задача может быть только отклонена.
Весь процесс можно представить следующей картинкой:
2. Создайте рабочий поток
Создание рабочего потока требует ряда суждений.Необходимо убедиться, что текущий пул потоков может создать новый поток, прежде чем его можно будет создать.
Во-первых, когда состояние пула потоков SHUTDOWN или STOP, новые потоки не могут быть созданы.
Кроме того, если фабрике потоков не удается создать поток, он не может создать новый поток.
Кроме того, количество текущих рабочих потоков сравнивается с количеством основных потоков и максимальным количеством потоков, если первое больше второго, создание не разрешается.
Кроме того, он попытается самостоятельно увеличить количество рабочих потоков через CAS.Если самоувеличение пройдет успешно, будет создан новый рабочий поток, то есть объект Worker.
Затем заблокируйте второй раз, чтобы проверить, можно ли создать рабочий поток, и, наконец, если создание прошло успешно, рабочий поток будет запущен.
3. Запустите рабочий поток
Когда рабочий поток успешно создан, то есть объект Worker был создан, вам нужно запустить рабочий поток и позволить потоку начать работу.Существует поток, связанный с объектом Worker, поэтому, если вы хотите запустить рабочий поток, вам нужно только передать рабочий .thread.start(), чтобы запустить поток.
После запуска будет выполнен метод run объекта Worker.Поскольку Worker реализует интерфейс Runnable, Worker по сути представляет собой поток.
После открытия потока запуска будет вызван метод run объекта Runnable, в методе run объекта worker вызывается метод runWorker(this), то есть в метод runWorker передается текущий объект, так что его можно выполнить.
4. Получить задание и выполнить его
После вызова метода runWorker он должен выполнить конкретную задачу.Во-первых, вам нужно получить исполняемую задачу, а задача по умолчанию привязана к объекту Worker.Если задача не пуста, она выполняется напрямую.
После завершения выполнения вы перейдете к задаче очереди блокировки для выполнения и получите задачу процесса, необходимо учитывать текущее количество рабочих потоков.
Если количество рабочих потоков больше, чем количество основных потоков, то его нужно получить через опрос, потому что в это время неиспользуемые потоки необходимо перезапустить;
Если количество рабочих потоков меньше или равно количеству основных потоков, то его можно получить с помощью take, поэтому все потоки в это время являются основными потоками, и их не нужно перезапускать, при условии, что не установлен параметр allowCoreThreadTimeOut.