Зазвичай під час розробки багатопотокового застосунку ми стикаємося з організацією роботи потоків. Чим більший наш застосунок і чим більше потоків нам потрібно для організації виконання багатопотокових завдань, тим більше об'єктів Runnable ми створюємо.

Тут варто зазначити, що створення потоку в Java — доволі дорога операція. Якщо ми будемо створювати новий екземпляр потоку кожного разу для виконання операції, ми матимемо величезні проблеми з продуктивністю і в результаті — з працездатністю застосунку.

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

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

Якщо завдань більше, ніж потоків, то вони очікують в черзі (Task Queue). З черги завдання попадає на виконання до N-ного потоку з пула, а після виконання завдання потік забирає нове завдання з черги. Після виконання всіх завдань із черги потоки залишаються активними і чекають на нові завдання. Коли завдання з'являються, потоки починають виконувати і їх.

ThreadPoolExecutor

Із 5-ої версії Java у вирішенні багатопоточності з'являється 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);
                    

Кожен із видів пулів ми розглянемо у наступних лекціях.