Как изящно использовать пул потоков

Java

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

процесс

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

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

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

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

нить

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

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

данные в треде

  1. локальные данные в стеке потоков: Например, локальные переменные процесса выполнения функции, мы знаем, что модель потока в Java — это модель, использующая стек. Каждый поток имеет собственное пространство стека.
  2. Глобальные данные, совместно используемые процессами: Мы знаем, что в Java-программе Java — это процесс, мы можем передатьps -ef | grep javaВы можете увидеть, сколько Java-процессов запущено в программе, например глобальных переменных в нашей Java, которые изолированы между разными процессами, но совместно используются потоками.
  3. личные данные потока: В Java мы можем передатьThreadLocalдля создания переменных данных, которые являются частными между потоками.

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

Привязка к ЦП и привязка к вводу-выводу

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

  • Интенсивный ввод-вывод: ЦП большую часть времени простаивает, ожидая дисковых операций ввода-вывода.
  • Интенсивность ЦП (вычислений): большую часть времени дисковый ввод-вывод простаивает, ожидая вычислительных операций ЦП.

Пул потоков

Пул потоков на самом деле является приложением технологии пула.Существует много распространенных технологий пула, таких как пул соединений с базой данных, пул памяти в Java, постоянный пул и так далее. И почему существует технология пулинга? Суть работы программы заключается в полной обработке информации с использованием системных ресурсов (ЦП, памяти, сети, диска и т.д.) Например, создание экземпляра объекта в JVM требует потребления ресурсов ЦП и памяти. Если вашей программе необходимо часто создавать большое количество объектов, а короткое время существования этих объектов означает, что их нужно часто уничтожать, очень вероятно, что этот код станет узким местом в производительности. Подводя итог, это на самом деле следующие моменты.

  • Повторное использование одних и тех же ресурсов, сокращение отходов и снижение стоимости нового строительства и разрушения;
  • Сократить расходы на раздельное управление и передать в «пул» равномерно;
  • Централизованное управление для уменьшения «фрагментации»;
  • Улучшить скорость отклика системы, поскольку в пуле есть существующие ресурсы, и нет необходимости их пересоздавать;

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

Четыре пула потоков, предоставляемые Java

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

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

    1public static ExecutorService newCachedThreadPool() {
    2    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    3                                  60L, TimeUnit.SECONDS,
    4                                  new SynchronousQueue<Runnable>());
    5}
  • Executors.newFixedThreadPool: создайте пул потоков фиксированного размера, который может управлять максимальным числом одновременных потоков, а лишние потоки будут ожидать в очереди. Проще говоря, задачи будут помещены в очередь бесконечной длины, когда они не заняты.

    1   public static ExecutorService newFixedThreadPool(int nThreads) {
    2    return new ThreadPoolExecutor(nThreads, nThreads,
    3                                  0L, TimeUnit.MILLISECONDS,
    4                                  new LinkedBlockingQueue<Runnable>());
    5}
  • Executors.newSingleThreadExecutor: Создайте пул потоков с номером потока 1 в пуле потоков, используйте уникальный поток для выполнения задач и убедитесь, что задачи выполняются в указанном порядке.

    1public static ExecutorService newSingleThreadExecutor() {
    2    return new FinalizableDelegatedExecutorService
    3        (new ThreadPoolExecutor(1, 1,
    4                                0L, TimeUnit.MILLISECONDS,
    5                                new LinkedBlockingQueue<Runnable>()));
    6}
  • Executors.newScheduledThreadPool: создание пула потоков фиксированного размера для поддержки запланированного и периодического выполнения задач.

    1public ScheduledThreadPoolExecutor(int corePoolSize) {
    2    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
    3          new DelayedWorkQueue());
    4}

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

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

1public ThreadPoolExecutor(int corePoolSize,
2                              int maximumPoolSize,
3                              long keepAliveTime,
4                              TimeUnit unit,
5                              BlockingQueue<Runnable> workQueue,
6                              ThreadFactory threadFactory,
7                              RejectedExecutionHandler handler)
8

Итак, что именно означают эти параметры?

  • corePoolSize: количество основных потоков в пуле потоков.
  • maximumPoolSize: максимальное количество потоков, разрешенных в пуле потоков.
  • keepAliveTime: когда количество существующих потоков больше, чемcorePoolSize, то будет найден незанятый поток для уничтожения.Этот параметр указывает, как долго простаивающий поток будет уничтожаться.
  • unit: единица времени
  • workQueue: рабочая очередь, если текущее количество потоков в пуле потоков больше, чем количество основных потоков, следующая задача будет помещена в очередь.
  • threadFactory: при создании резьбы используйте фабричный образец для изготовления нитей. Этот параметр должен установить нашу собственную фабрику создания потоков.
  • handler: если превышено максимальное количество потоков, будет выполнена установленная нами политика отклонения.

Затем мы объединяем эти параметры, чтобы увидеть, какова их логика обработки.

  1. впередcorePoolSizeКогда приходит задача, создается задача и создается поток
  2. Если количество потоков в текущем пуле потоков превышаетcorePoolSizeЗатем следующая задача будет помещена в настройки, которые мы установили выше.workQueueв очереди
  3. если в это времяworkQueueОн тоже полный, то при повторном приходе задачи будет создаваться новый временный поток, то если мы установимkeepAliveTimeили установитьallowCoreThreadTimeOut, тогда система проверит активность потока и уничтожит поток, когда истечет время ожидания.
  4. Если текущий поток в пуле потоков больше, чемmaximumPoolSizeМаксимальное количество потоков, тогда будет выполняться то, что мы только что установилиhandlerполитика отказа

Почему не рекомендуется использовать метод создания пула потоков, предоставляемый Java?

Разобравшись с установленными выше параметрами, давайте посмотрим, почему в «Руководстве по Alibaba Java» есть такое положение.

Я считаю, что, ознакомившись с четырьмя приведенными выше принципами реализации для создания пулов потоков, вы должны понять, почему у Alibaba есть такие правила.

  • FixedThreadPoolиSingleThreadExecutor: Реализация этих двух пулов потоков, мы можем видеть, что рабочие очереди, которые он устанавливает, всеLinkedBlockingQueue, мы знаем, что эта очередь представляет собой очередь в виде связанного списка. Эта очередь не имеет ограничения по длине и является неограниченной очередью. Если в это время имеется большое количество запросов, это может вызватьOOM.
  • CachedThreadPoolиScheduledThreadPool: Реализация этих двух пулов потоков, мы можем видеть, что максимальное количество потоков, которое он устанавливает, равноInteger.MAX_VALUE, то это эквивалентно количеству потоков, которые можно создать какInteger.MAX_VALUE. В это время, если есть большое количество запросов, это также может вызватьOOM.

Как установить параметры

Поэтому, если мы хотим использовать пулы потоков в наших проектах, мы рекомендуем создавать пулы потоков индивидуально, исходя из условий наших собственных проектов и машин. Итак, как установить эти параметры? Чтобы правильно настроить длину пула потоков, необходимо понимать конфигурацию вашего компьютера, ситуацию с требуемыми ресурсами и особенности задачи. Например, сколько процессоров установлено на развернутом компьютере? Сколько памяти? Основное выполнение задачи интенсивно использует операции ввода-вывода или ЦП? Требует ли выполняемая задача дефицитного ресурса, такого как подключение к базе данных?

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

  • Задачи с интенсивным использованием ЦП: это указывает на то, что задействовано большое количество вычислительных операций.Например, если имеется N ЦП, настройте емкость пула потоков на N+1, чтобы получить оптимальное использование. Поскольку поток, интенсивно использующий ЦП, в какой-то момент приостанавливается из-за сбоя страницы или по какой-либо другой причине, наличие всего одного дополнительного потока гарантирует, что в этом случае циклы ЦП не прерывают работу.

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

    • N: количество процессоров
    • U: целевое использование ЦП, 0
    • W/C: отношение времени ожидания ко времени вычисления
    • Тогда оптимальный размер пулаNU(1+W/C)

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

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

Разрушение потока в пуле потоков

Создание и уничтожение потоков совместно управляются количеством основных потоков пула потоков (corePoolSize), максимальным количеством потоков (maximumPoolSize) и временем существования потоков (keepAliveTime). Далее рассмотрим, как пул потоков создает и уничтожает потоки.

  • Текущее количество потоков
  • Текущее количество потоков = количеству основных потоков: задача будет добавлена ​​в очередь
  • Текущее количество потоков > количество основных потоков: в это время есть предварительное условие, что очередь заполнена, и будет создан новый поток, в это время будет включена проверка активности потока.keepAliveTimeПотоки, которые не активны какое-то время, будут переработаны

Тогда кто-то здесь может подумать оcorePoolSizeКоличество основных потоков установлено равным 0 (Если вы помните, что я сказал вышеCachedThreadPoolЕсли это так, вы все равно должны помнить, что количество основных потоков равно 0.), потому что, если это установлено, поток будет создаваться динамически, не будет потока, когда он простаивает, и поток будет создан в пуле потоков, когда он занят. Эта идея хороша, но если наш пользовательский параметр устанавливает этот параметр в 0, а просто устанавливает очередь ожидания, это не так.SynchronousQueue, тогда на самом деле будет проблема, потому что новый поток будет создан только тогда, когда очередь будет заполнена. Ниже кода я использую неограниченную очередьLinkedBlockingQueue, собственно, посмотрим на вывод

 1ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0,Integer.MAX_VALUE,1, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
2for (int i = 0; i < 10; i++) {
3    threadPoolExecutor.execute(new Runnable() {
4        @Override
5        public void run() {
6            try {
7                Thread.sleep(1000);
8            } catch (InterruptedException e) {
9                e.printStackTrace();
10            }
11            System.out.printf("1");
12        }
13    });
14}

Вы можете увидеть эффект демонстрации, на самом деле1Он печатается каждую секунду На самом деле это противоречит нашему первоначальному намерению использовать пул потоков, потому что наш эквивалентен работе в одном потоке.

Но если мы заменим рабочую очередь наSynchronousQueueНу, мы нашли эти1является частью вывода.

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

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

Политика отказа в пуле потоков

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

1private static final RejectedExecutionHandler defaultHandler =
2    new AbortPolicy();

Мы можем перейти к запрету политики, это интерфейсRejectedExecutionHandler, что означает, что мы можем установить нашу собственную стратегию отклонения.Давайте сначала посмотрим, что предоставляет Java из четырех стратегий отклонения.

 1public interface RejectedExecutionHandler {
2
3    /**
4     * Method that may be invoked by a {@link ThreadPoolExecutor} when
5     * {@link ThreadPoolExecutor#execute execute} cannot accept a
6     * task.  This may occur when no more threads or queue slots are
7     * available because their bounds would be exceeded, or upon
8     * shutdown of the Executor.
9     *
10     * <p>In the absence of other alternatives, the method may throw
11     * an unchecked {@link RejectedExecutionException}, which will be
12     * propagated to the caller of {@code execute}.
13     *
14     * @param r the runnable task requested to be executed
15     * @param executor the executor attempting to execute this task
16     * @throws RejectedExecutionException if there is no remedy
17     */
18    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
19}

AbortPolicy

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

 1public static class AbortPolicy implements RejectedExecutionHandler {
2
3    public AbortPolicy() { }
4
5    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
6        throw new RejectedExecutionException("Task " + r.toString() +
7                                             " rejected from " +
8                                             e.toString());
9    }
10}

Таким образом, стратегия отказа состоит в том, чтобы броситьRejectedExecutionExceptionаномальный

CallerRunsPolicy

Эта стратегия отказа просто означает, что задача передается вызывающей стороне для непосредственного выполнения.

 1public static class CallerRunsPolicy implements RejectedExecutionHandler {
2
3    public CallerRunsPolicy() { }
4
5    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
6        if (!e.isShutdown()) {
7            r.run();
8        }
9    }
10}

Почему он передается вызывающей стороне для выполнения? Мы видим, что он называетсяrun()метод вместоstart()метод.

DiscardOldestPolicy

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

 1public static class DiscardOldestPolicy implements RejectedExecutionHandler {
2
3        public DiscardOldestPolicy() { }
4
5        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
6            if (!e.isShutdown()) {
7                e.getQueue().poll();
8                e.execute(r);
9            }
10        }
11    }

DiscardPolicy

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

1public static class DiscardPolicy implements RejectedExecutionHandler {
2
3    public DiscardPolicy() { }
4
5    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
6    }
7}

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

тупик голодания потока

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

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

 1public class AboutThread {
2    ExecutorService executorService = Executors.newSingleThreadExecutor();
3    public static void main(String[] args) {
4        AboutThread aboutThread = new AboutThread();
5        aboutThread.threadDeadLock();
6    }
7
8    public void threadDeadLock(){
9        Future<String> taskOne  = executorService.submit(new TaskOne());
10        try {
11            System.out.printf(taskOne.get());
12        } catch (InterruptedException e) {
13            e.printStackTrace();
14        } catch (ExecutionException e) {
15            e.printStackTrace();
16        }
17    }
18
19    public class TaskOne implements Callable{
20
21        @Override
22        public Object call() throws Exception {
23            Future<String> taskTow = executorService.submit(new TaskTwo());
24            return "TaskOne" + taskTow.get();
25        }
26    }
27
28    public class TaskTwo implements Callable{
29
30        @Override
31        public Object call() throws Exception {
32            return "TaskTwo";
33        }
34    }
35}

Расширить ThreadPoolExecutor

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

фабрика нитей

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

 1public interface ThreadFactory {
2
3    /**
4     * Constructs a new {@code Thread}.  Implementations may also initialize
5     * priority, name, daemon status, {@code ThreadGroup}, etc.
6     *
7     * @param r a runnable to be executed by new thread instance
8     * @return constructed thread, or {@code null} if the request to
9     *         create a thread is rejected
10     */
11    Thread newThread(Runnable r);
12}

Далее мы можем взглянуть на класс фабрики пула потоков, который мы написали сами.

 1class CustomerThreadFactory implements ThreadFactory{
2
3    private String name;
4    private final AtomicInteger threadNumber = new AtomicInteger(1);
5    CustomerThreadFactory(String name){
6        this.name = name;
7    }
8
9    @Override
10    public Thread newThread(Runnable r) {
11        Thread thread = new Thread(r,name+threadNumber.getAndIncrement());
12        return thread;
13    }
14}

Просто добавьте этот фабричный класс при создании экземпляра пула потоков

 1   public static void customerThread(){
2        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0,Integer.MAX_VALUE,1, TimeUnit.SECONDS,new SynchronousQueue<>(),
3                new CustomerThreadFactory("customerThread"));
4
5        for (int i = 0; i < 10; i++) {
6            threadPoolExecutor.execute(new Runnable() {
7                @Override
8                public void run() {
9                    System.out.printf(Thread.currentThread().getName());
10                    System.out.printf("\n");
11                }
12            });
13        }
14    }

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

 1customerThread1
2customerThread10
3customerThread9
4customerThread8
5customerThread7
6customerThread6
7customerThread5
8customerThread4
9customerThread3
10customerThread2

Расширение путем создания подкласса ThreadPoolExecutor

мы смотримThreadPoolExecutorВ исходном коде можно найти, что в исходном коде есть три метода.protected

1protected void beforeExecute(Thread t, Runnable r) { }
2protected void afterExecute(Runnable r, Throwable t) { }
3protected void terminated() { }

Члены, измененные с помощью protected, видны этому пакету и его подклассам.

Мы можем переопределить эти методы через наследование, чтобы мы могли создавать свои собственные расширения. Поток, выполняющий задачу, вызоветbeforeExecuteиafterExecuteметоды, с помощью которых вы можете добавить возможности ведения журналов, временных рядов, мониторинга или однорангового сбора информации. Независимо от того, возвращается ли задача из режима нормального выполнения или выдает исключение,afterExecuteбудет называться(Если задача завершается и выдает ошибку, тоafterExecuteне будет называться). еслиbeforeExecuteбросатьRuntimeException, задача не будет выполнена,afterExecuteтоже не позовут.

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

Кодовый адрес этой статьи

Если вам интересно, можете обратить внимание на мой новый паблик аккаунт и поискать [Сумка с сокровищами программиста]. Или просто отсканируйте код ниже.

Ссылаться на