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

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

Тут нам на помощь приходит Thread pool (пул потоков) и ThreadPoolExecutor.

Thread pool — это набор предварительно инициализированных потоков, размер которого может быть как фиксированным, так и переменным.

Если задач больше, чем потоков, то задачи ждут в очереди (Task Queue). Из очереди задача попадает на исполнение к N-ному потоку из пула, а после выполнения задачи поток забирает новую задачу из очереди. После выполнения всех задач из очереди потоки остаются активными и ждут новых задач. Когда задачи появляются, потоки начинают выполнять и их.

ThreadPoolExecutor

Начиная с Java 5 в решении многопоточности появляется Executor framework. В целом он имеет множество компонентов и пришел к нам, чтобы решить проблему эффективного управления очередью и пулом потоков.

Главные интерфейсы — Executor и ExecutorService.

Executor — интерфейс с единственным методом void execute(Runnable runnable).

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

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

ThreadPoolExecutor реализует интерфейсы Executor и ExecutorService и разделяет создание задачи и ее выполнение. Нам необходимо реализовать объекты Runnable и отправить их исполнителю, а ThreadPoolExecutor отвечает за их исполнение, создание экземпляров и работу с потоками.

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

ThreadPoolExecutor имеет 4 конструктора:


ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime, 
TimeUnit unit, 
BlockingQueue<Runnable> workQueue)
    

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

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

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

В конструктор передаются параметры ThreadPoolExecutor:

corePoolSize Параметр, который показывает, какое количество потоков будет готово (запущено) при старте executor сервиса.
maximumPoolSize Максимальное количество потоков, которое может создать executor сервис.
keepAliveTime Время, в течение которого освободившийся поток будет жить и впоследствии будет уничтожен, если количество потоков больше corePoolSize. Временные единицы указываются в следующем параметре.
unit Времненные единицы (часы, минуты, секунды, милисекунды и так далее).
workQueue Реализация очереди для задач.
handler Обработчик задач, которые не могут быть выполнены.
threadFactory Объект, который создает новые потоки по требованию. Использование фабрик потоков устраняет аппаратную привязку вызовов к новому потоку, позволяя приложениям использовать специальные подклассы потоков, приоритеты и т. д.

Создание ThreadPoolExecutor

Создание ThreadPoolExecutor может нам упростить утилитарный класс Executors. В этом утилитарном классе есть методы, которые нам помогут подготовить объект ThreadPoolExecutor.

newFixedThreadPool — создает пул потоков, который повторно использует фиксированное количество потоков для выполнения любого количества задач.

ExecutorService executor = Executors.newFixedThreadPool(10);
                    
newWorkStealingPool – создает пул потоков, где количество потоков = количество ядер процессора, которые доступны для JVM. Дефолтный уровень параллелизма – один. Это значит, что в пуле будет создано столько потоков, сколько ядер ЦП доступно JVM. Если параллелизм равен 4, тогда вместо количества ядер используется переданное значение.

ExecutorService executor = Executors.newWorkStealingPool(4);
                    
newSingleThreadExecutor – создает пул с единственным потоком для выполнения всех задач.

ExecutorService executor = Executors.newSingleThreadExecutor();
                    
newCachedThreadPool – создает пул потоков, который создает новые потоки по мере необходимости, но будет повторно использовать ранее созданные потоки, когда они будут доступны.

ExecutorService executor = Executors.newCachedThreadPool();
                    
newScheduledThreadPool – создает пул потоков, который может планировать выполнение команд после заданной задержки или для периодического выполнения.

ScheduledExecutorService executor = Executors.newScheduledThreadPool(10);
                    

Каждый из видов пулов мы рассмотрим в следующих лекциях.