Лучшие практики пула потоков! договариваться!

Java

Всем привет, я Brother Guide, технический человек с тремя взглядами, которые более позитивны, чем главный герой. Давайте сегодня снова поговорим о пуле потоков ~

Рекомендации по пулу потоков

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

Обзор знаний о пуле потоков

Прежде чем начать эту статью, я кратко представлю пул потоков, о котором писал ранее.«Сводка обучения пулу потоков, которую могут понять новички»Эта статья очень подробная.

Зачем использовать пул потоков?

"

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

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

Вот цитата из "Искусство параллельного программирования на Java"Преимущества использования пулов потоков:

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

Сценарии использования пула потоков в реальных проектах

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

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

Уведомление:Следующие три задачи могут делать одно и то же, а могут быть разными.

使用线程池前后对比
До и после использования пула потоков

Как использовать пул потоков?

обычно черезThreadPoolExecutorКонструктор для создания пула потоков, а затем отправки задач в пул потоков для выполнения.

ThreadPoolExecutorКонструктор выглядит следующим образом:

    /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

Краткая демонстрация того, как использовать пул потоков, для более подробного ознакомления см.:«Сводка обучения пулу потоков, которую могут понять новички».

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {

        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            executor.execute(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("CurrentThread name:" + Thread.currentThread().getName() + "date:" + Instant.now());
            });
        }
        //终止线程池
        executor.shutdown();
        try {
            executor.awaitTermination(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Finished all threads");
    }

Вывод консоли:

CurrentThread name:pool-1-thread-5date:2020-06-06T11:45:31.639Z
CurrentThread name:pool-1-thread-3date:2020-06-06T11:45:31.639Z
CurrentThread name:pool-1-thread-1date:2020-06-06T11:45:31.636Z
CurrentThread name:pool-1-thread-4date:2020-06-06T11:45:31.639Z
CurrentThread name:pool-1-thread-2date:2020-06-06T11:45:31.639Z
CurrentThread name:pool-1-thread-2date:2020-06-06T11:45:33.656Z
CurrentThread name:pool-1-thread-4date:2020-06-06T11:45:33.656Z
CurrentThread name:pool-1-thread-1date:2020-06-06T11:45:33.656Z
CurrentThread name:pool-1-thread-3date:2020-06-06T11:45:33.656Z
CurrentThread name:pool-1-thread-5date:2020-06-06T11:45:33.656Z
Finished all threads

Рекомендации по пулу потоков

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

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

1. ИспользуйтеThreadPoolExecutorКонструктор объявляет пул потоков

1. Пул потоков должен быть передан вручнуюThreadPoolExecutorконструктор для объявления, избегайте использованияExecutorsКатегорияnewFixedThreadPoolиnewCachedThreadPool, потому что может быть риск OOM.

"

Недостатки Executors, возвращающие объекты пула потоков, заключаются в следующем:

  • FixedThreadPoolиSingleThreadExecutor: допустимая длина очереди для запросовInteger.MAX_VALUE, может накапливаться большое количество запросов, что приводит к OOM.
  • CachedThreadPool и ScheduledThreadPool: Количество потоков, разрешенных для создания, равноInteger.MAX_VALUE, потенциально создавая большое количество потоков, что приводит к OOM.

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

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

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

2. Следите за состоянием работы пула потоков.

Вы можете определить текущее состояние пула потоков с помощью некоторых средств, таких как компонент Actuator в SpringBoot.

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

Ниже представлена ​​простая демонстрация.printThreadPoolStatus()Количество потоков в пуле потоков, количество активных потоков, количество выполненных задач и количество задач в очереди выводятся каждую секунду.

    /**
     * 打印线程池的状态
     *
     * @param threadPool 线程池对象
     */
    public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
        ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-thread-pool-status", false));
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            log.info("=========================");
            log.info("ThreadPool Size: [{}]", threadPool.getPoolSize());
            log.info("Active Threads: {}", threadPool.getActiveCount());
            log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount());
            log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
            log.info("=========================");
        }, 0, 1, TimeUnit.SECONDS);
    }

3. Разным типам бизнеса рекомендуется использовать разные пулы потоков.

У многих людей есть такие проблемы в реальных проектах:Несколько предприятий в моем проекте должны использовать пулы потоков. Должен ли я определить один для каждого пула потоков или определить общий пул потоков?

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

Давайте посмотрим на реальный случай аварии!(Этот случай взят из:«Онлайн-авария из-за неправильного использования пулов потоков», замечательный случай)

案例代码概览
Обзор кода дела

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

Рассмотрим этот крайний случай:

Если количество основных потоков в нашем пуле потоков равноn, количество родительских задач (задач дедукции) составляетn, есть две подзадачи под родительской задачей (подзадачи под задачей вывода), одна из которых выполнена, а другая поставлена ​​в очередь задач. Поскольку родительская задача израсходовала ресурсы основного потока пула потоков, дочерняя задача не может нормально выполняться, поскольку не может получить ресурсы потока и заблокирована в очереди. Родительская задача ожидает, пока дочерняя задача завершит выполнение, а дочерняя задача ожидает, пока родительская задача освободит ресурсы пула потоков, что также приводит к«Тупик».

Решение также очень простое: добавить новый пул потоков для выполнения подзадач для его обслуживания.

4. Не забудьте назвать пул потоков

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

Имя потока, созданное по умолчанию, похоже на pool-1-thread-n, что не имеет бизнес-значения и не способствует обнаружению проблемы.

Обычно существует два способа назвать потоки в пуле потоков:

**1. Использование гуавыThreadFactoryBuilder **

ThreadFactory threadFactory = new ThreadFactoryBuilder()
                        .setNameFormat(threadNamePrefix + "-%d")
                        .setDaemon(true).build();
ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory)

2. Сделай самThreadFactor.

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * 线程工厂,它设置线程名称,有利于我们定位问题。
 */
public final class NamingThreadFactory implements ThreadFactory {

    private final AtomicInteger threadNum = new AtomicInteger();
    private final ThreadFactory delegate;
    private final String name;

    /**
     * 创建一个带名字的线程池生产工厂
     */
    public NamingThreadFactory(ThreadFactory delegate, String name) {
        this.delegate = delegate;
        this.name = name; // TODO consider uniquifying this
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = delegate.newThread(r);
        t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
        return t;
    }

}

5. Правильно настроить параметры пула потоков

Когда дело доходит до настройки параметров пула потоков, операция Meituan show была для меня незабываемой (будет упомянуто позже)!

Давайте сначала рассмотрим общие рекомендуемые способы настройки параметров пула потоков в различных книгах и блогах, которые можно использовать в качестве справки!

Нормальная операция

Многие люди могут даже подумать, что лучше настроить пул потоков слишком большим! Я думаю, что это явно проблема. Возьмем очень распространенный пример из нашей жизни:Дело не в том, что многие люди могут делать что-то хорошо, что увеличивает стоимость общения. Изначально вам нужно было только 3 человека для выполнения одной задачи, но вы только что привлекли 6 человек, повысит ли это эффективность работы? Я так не думаю.Влияние слишком большого количества потоков такое же, как и количество людей, которых мы назначаем для выполнения задач.Для многопоточного сценария оно в основном увеличивается.переключатель контекстаСтоимость. Если вы не знаете, что такое переключение контекста, вы можете прочитать мое введение ниже.

"

Переключение контекста:

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

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

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

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

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

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

Существует простая и широко применимая формула:

  • Задачи с интенсивным использованием ЦП (N+1):Эта задача в основном потребляет ресурсы ЦП.Количество потоков может быть установлено равным N (количество ядер ЦП) + 1. На один поток больше, чем число ядер ЦП, чтобы предотвратить случайное прерывание потока из-за ошибки страницы, или задача вызвано другими причинами воздействия подвески. Как только задача будет приостановлена, ЦП будет бездействовать, и дополнительный поток в этом случае может полностью использовать время простоя ЦП.
  • Задачи с интенсивным вводом-выводом (2N):Когда применяется задача такого типа, система будет тратить большую часть времени на обработку взаимодействия ввода-вывода, и поток не будет занимать ЦП для обработки в течение периода времени обработки ввода-вывода.В это время ЦП может быть передан в пользование другим потокам. . Таким образом, при выполнении задач с интенсивным вводом-выводом мы можем настроить больше потоков, а конкретный метод расчета — 2N.

Как определить, является ли это задачей с интенсивным использованием ЦП или задачей с интенсивным вводом-выводом?

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

Операция Мэйтуана в Сао

Техническая команда Meituan находится в«Принцип реализации пула потоков Java и его практика в бизнесе Meituan»В этой статье представлены идеи и методы настраиваемой конфигурации параметров пула потоков.

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

  • corePoolSize :Основные потоки Потоки определяют минимальное количество потоков, которые могут выполняться одновременно.
  • maximumPoolSize :Когда задачи, хранящиеся в очереди, достигают емкости очереди, текущее количество потоков, которые могут выполняться одновременно, становится максимальным количеством потоков.
  • workQueue:Когда приходит новая задача, она сначала определяет, достигло ли количество запущенных в данный момент потоков количества основных потоков, и если да, то доверие сохраняется в очереди.

Почему эти три параметра?

я здесь«Сводка обучения пулу потоков, которую могут понять новички»Говорят, что эти три параметраThreadPoolExecutorСамые важные параметры, они в основном определяют стратегию обработки задач пулом потоков.

Как поддерживать динамическую настройку параметров?ПосмотримThreadPoolExecutorПредусмотрены следующие методы.

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

Кроме того, вы также видели, что нет способа динамически указать длину очереди выше.Метод Meituan заключается в настройке метода, называемогоResizableCapacityLinkedBlockIngQueueочередь (в основном ставлюLinkedBlockingQueueПоследний модификатор ключевого слова поля емкости удален, что делает его изменяемым).

Окончательный эффект динамически изменяемых параметров пула потоков следующий. 👏👏👏

动态配置线程池参数最终效果
Динамически настроить окончательный эффект параметров пула потоков

Не видели достаточно? порекомендуй, почему бог"Как установить параметры пула потоков? Мейтуан дал ответ, который шокировал интервьюера. 》Эта статья, глубокий анализ, очень хороша!

об авторе:Звездный проект Github 80kJavaGuide(Публичный аккаунт с таким же именем) Автор. Каждую неделю я буду обновлять некоторые из моих собственных оригинальных галантерейных товаров в общественном аккаунте. Ответьте "1" на фоне официального аккаунта, чтобы получить необходимые учебные материалы для инженеров Java + штурм интервью pdf.

В этой статье используетсяmdniceнабор текста