Виконавці та Пул Потоків
Ми вже розглянули різноманітні механізми підтримки багатопотоковості, і Executors
є одним із них!
Що таке Executors і пул потоків?
Executors
— це механізм, який надає високорівневі абстракції для роботи з потоками. Він дозволяє створювати та керувати пулом потоків, який складається з набору заздалегідь створених потоків, готових до виконання завдань. Замість створення нового потоку для кожного завдання, завдання надсилаються до пулу, де їх виконання розподіляється між потоками.
Що ж таке пул потоків? Це колекція заздалегідь створених потоків, готових виконувати завдання. Використовуючи пул потоків, ви уникаєте витрат на створення та знищення потоків щоразу, оскільки одні й ті самі потоки можуть використовуватися для декількох завдань.
Якщо завдань більше, ніж потоків, завдання очікують у Task Queue
. Завдання з черги обробляється доступним потоком з пулу, і після завершення завдання потік бере нове завдання з черги. Коли всі завдання в черзі виконані, потоки залишаються активними та очікують нових завдань.
Приклад з життя
Уявіть ресторан, де кухарі (потоки) готують замовлення (завдання). Замість того, щоб наймати нового кухаря для кожного замовлення, ресторан утримує обмежену кількість кухарів, які виконують замовлення по мірі їх надходження. Коли один кухар завершує замовлення, він береться за наступне, що дозволяє ефективно використовувати ресурси ресторану.
Основний метод
newFixedThreadPool(int n)
: Створює пул із фіксованою кількістю потоків, що дорівнює n.
Main.java
1ExecutorService executorService = Executors.newFixedThreadPool(20);
newCachedThreadPool()
: Створює пул, який може створювати нові потоки за потреби, але повторно використовує доступні потоки, якщо такі є.
Main.java
1ExecutorService executorService = Executors.newCachedThreadPool();
newSingleThreadExecutor()
: Створює пул з одним потоком, який гарантує, що завдання виконуються послідовно, тобто одне за одним. Корисно для завдань, які мають виконуватися у суворій послідовності.
Main.java
1ExecutorService executorService = Executors.newSingleThreadExecutor();
У всіх наведених прикладах методи класу Executors
повертають реалізацію інтерфейсу ExecutorService
, який використовується для керування потоками.
ExecutorService
надає методи для керування пулом потоків. Наприклад, submit(Runnable task)
приймає задачу у вигляді об'єкта Runnable
і розміщує її в черзі на виконання. Він повертає об'єкт Future
, який можна використовувати для перевірки стану задачі та отримання результату, якщо задача повертає результат.
Main.java
12345678910111213141516171819202122232425262728293031323334package com.example; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { // Create a thread pool with 5 threads ExecutorService executor = Executors.newFixedThreadPool(5); // Define the task to be executed Runnable task = () -> { System.out.println("Task is running: " + Thread.currentThread().getName()); try { Thread.sleep(2000); // Simulate some work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task completed: " + Thread.currentThread().getName()); }; // Submit the task for execution and get a `Future` Future<?> future = executor.submit(task); // Check if the task is done System.out.println("Is task done? " + future.isDone()); // You can use `future` to check the status of the task or wait for its completion // Example: future.get() - blocks until the task is completed (not used in this example) // Initiate an orderly shutdown of the executor service executor.shutdown(); } }
Метод shutdown()
ініціює коректне завершення роботи пулу потоків. Він припиняє прийом нових задач, але завершує поточні задачі. Після виклику цього методу пул не можна перезапустити.
Метод awaitTermination(long timeout, TimeUnit unit)
очікує завершення всіх задач у пулі протягом заданого часу. Це блокуюче очікування, яке дозволяє переконатися, що всі задачі виконані перед остаточним завершенням роботи пулу.
Також ми не згадували про основний інтерфейс, який допомагає відстежувати стан потоку, це інтерфейс Future
. Метод submit()
інтерфейсу ExecutorService
повертає реалізацію інтерфейсу Future
.
Якщо потрібно отримати результат виконання потоку, можна використати метод get()
. Якщо потік реалізує Runnable
, метод get()
нічого не повертає, але якщо Callable<T>
, він повертає тип T
.
Main.java
12345678910111213141516171819202122232425262728293031package com.example; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // Callable task that returns a result Callable<String> task = () -> { Thread.sleep(1000); // Simulate some work return "Task result"; }; Future<String> future = executor.submit(task); try { // Get the result of the task String result = future.get(); System.out.println("Task completed with result: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); } }
Також можна використати метод cancel(boolean mayInterruptIfRunning)
, щоб спробувати скасувати виконання завдання. Якщо завдання ще не розпочато, воно буде скасоване. Якщо завдання вже виконується, воно може бути перерване залежно від прапорця mayInterruptIfRunning
.
true
: Якщо завдання виконується, воно буде перерване шляхом виклику Thread.interrupt() у потоці, що виконує завдання.
false
: Якщо завдання виконується, воно не буде перерване, і спроба скасування не вплине на поточне виконання завдання.
А також 2 методи, інтуїтивно зрозумілі за своєю функцією:
isCancelled()
: Перевіряє, чи було скасовано завдання;isDone()
: Перевіряє, чи було завершено завдання.
Приклад використання
Розмір пулу потоків залежить від характеру виконуваних завдань. Зазвичай розмір пулу потоків не слід жорстко задавати; натомість він має бути налаштовуваним. Оптимальний розмір визначається шляхом моніторингу пропускної здатності виконуваних завдань.
Максимально ефективно використовувати кількість threads = processor cores
. Це можна побачити у коді за допомогою Runtime.getRuntime().availableProcessors()
.
Main.java
1int availableProcessors = Runtime.getRuntime().availableProcessors();
Відмінності між безпосереднім створенням потоків і використанням ExecutorService
Основні відмінності між безпосереднім створенням потоків і використанням ExecutorService
полягають у зручності та керуванні ресурсами. Ручне створення потоків вимагає індивідуального керування кожним потоком, що ускладнює код і адміністрування.
ExecutorService
спрощує керування за рахунок використання пулу потоків, що полегшує обробку завдань. Крім того, ручне створення потоків може призвести до високого споживання ресурсів, тоді як ExecutorService
дозволяє налаштовувати розмір пулу потоків.
Дякуємо за ваш відгук!
Запитати АІ
Запитати АІ
Запитайте про що завгодно або спробуйте одне із запропонованих запитань, щоб почати наш чат
Can you explain more about how ExecutorService manages threads?
What are some best practices for using thread pools?
How does the Future interface help in managing task results?
Awesome!
Completion rate improved to 3.33
Виконавці та Пул Потоків
Свайпніть щоб показати меню
Ми вже розглянули різноманітні механізми підтримки багатопотоковості, і Executors
є одним із них!
Що таке Executors і пул потоків?
Executors
— це механізм, який надає високорівневі абстракції для роботи з потоками. Він дозволяє створювати та керувати пулом потоків, який складається з набору заздалегідь створених потоків, готових до виконання завдань. Замість створення нового потоку для кожного завдання, завдання надсилаються до пулу, де їх виконання розподіляється між потоками.
Що ж таке пул потоків? Це колекція заздалегідь створених потоків, готових виконувати завдання. Використовуючи пул потоків, ви уникаєте витрат на створення та знищення потоків щоразу, оскільки одні й ті самі потоки можуть використовуватися для декількох завдань.
Якщо завдань більше, ніж потоків, завдання очікують у Task Queue
. Завдання з черги обробляється доступним потоком з пулу, і після завершення завдання потік бере нове завдання з черги. Коли всі завдання в черзі виконані, потоки залишаються активними та очікують нових завдань.
Приклад з життя
Уявіть ресторан, де кухарі (потоки) готують замовлення (завдання). Замість того, щоб наймати нового кухаря для кожного замовлення, ресторан утримує обмежену кількість кухарів, які виконують замовлення по мірі їх надходження. Коли один кухар завершує замовлення, він береться за наступне, що дозволяє ефективно використовувати ресурси ресторану.
Основний метод
newFixedThreadPool(int n)
: Створює пул із фіксованою кількістю потоків, що дорівнює n.
Main.java
1ExecutorService executorService = Executors.newFixedThreadPool(20);
newCachedThreadPool()
: Створює пул, який може створювати нові потоки за потреби, але повторно використовує доступні потоки, якщо такі є.
Main.java
1ExecutorService executorService = Executors.newCachedThreadPool();
newSingleThreadExecutor()
: Створює пул з одним потоком, який гарантує, що завдання виконуються послідовно, тобто одне за одним. Корисно для завдань, які мають виконуватися у суворій послідовності.
Main.java
1ExecutorService executorService = Executors.newSingleThreadExecutor();
У всіх наведених прикладах методи класу Executors
повертають реалізацію інтерфейсу ExecutorService
, який використовується для керування потоками.
ExecutorService
надає методи для керування пулом потоків. Наприклад, submit(Runnable task)
приймає задачу у вигляді об'єкта Runnable
і розміщує її в черзі на виконання. Він повертає об'єкт Future
, який можна використовувати для перевірки стану задачі та отримання результату, якщо задача повертає результат.
Main.java
12345678910111213141516171819202122232425262728293031323334package com.example; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { // Create a thread pool with 5 threads ExecutorService executor = Executors.newFixedThreadPool(5); // Define the task to be executed Runnable task = () -> { System.out.println("Task is running: " + Thread.currentThread().getName()); try { Thread.sleep(2000); // Simulate some work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Task completed: " + Thread.currentThread().getName()); }; // Submit the task for execution and get a `Future` Future<?> future = executor.submit(task); // Check if the task is done System.out.println("Is task done? " + future.isDone()); // You can use `future` to check the status of the task or wait for its completion // Example: future.get() - blocks until the task is completed (not used in this example) // Initiate an orderly shutdown of the executor service executor.shutdown(); } }
Метод shutdown()
ініціює коректне завершення роботи пулу потоків. Він припиняє прийом нових задач, але завершує поточні задачі. Після виклику цього методу пул не можна перезапустити.
Метод awaitTermination(long timeout, TimeUnit unit)
очікує завершення всіх задач у пулі протягом заданого часу. Це блокуюче очікування, яке дозволяє переконатися, що всі задачі виконані перед остаточним завершенням роботи пулу.
Також ми не згадували про основний інтерфейс, який допомагає відстежувати стан потоку, це інтерфейс Future
. Метод submit()
інтерфейсу ExecutorService
повертає реалізацію інтерфейсу Future
.
Якщо потрібно отримати результат виконання потоку, можна використати метод get()
. Якщо потік реалізує Runnable
, метод get()
нічого не повертає, але якщо Callable<T>
, він повертає тип T
.
Main.java
12345678910111213141516171819202122232425262728293031package com.example; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); // Callable task that returns a result Callable<String> task = () -> { Thread.sleep(1000); // Simulate some work return "Task result"; }; Future<String> future = executor.submit(task); try { // Get the result of the task String result = future.get(); System.out.println("Task completed with result: " + result); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } executor.shutdown(); } }
Також можна використати метод cancel(boolean mayInterruptIfRunning)
, щоб спробувати скасувати виконання завдання. Якщо завдання ще не розпочато, воно буде скасоване. Якщо завдання вже виконується, воно може бути перерване залежно від прапорця mayInterruptIfRunning
.
true
: Якщо завдання виконується, воно буде перерване шляхом виклику Thread.interrupt() у потоці, що виконує завдання.
false
: Якщо завдання виконується, воно не буде перерване, і спроба скасування не вплине на поточне виконання завдання.
А також 2 методи, інтуїтивно зрозумілі за своєю функцією:
isCancelled()
: Перевіряє, чи було скасовано завдання;isDone()
: Перевіряє, чи було завершено завдання.
Приклад використання
Розмір пулу потоків залежить від характеру виконуваних завдань. Зазвичай розмір пулу потоків не слід жорстко задавати; натомість він має бути налаштовуваним. Оптимальний розмір визначається шляхом моніторингу пропускної здатності виконуваних завдань.
Максимально ефективно використовувати кількість threads = processor cores
. Це можна побачити у коді за допомогою Runtime.getRuntime().availableProcessors()
.
Main.java
1int availableProcessors = Runtime.getRuntime().availableProcessors();
Відмінності між безпосереднім створенням потоків і використанням ExecutorService
Основні відмінності між безпосереднім створенням потоків і використанням ExecutorService
полягають у зручності та керуванні ресурсами. Ручне створення потоків вимагає індивідуального керування кожним потоком, що ускладнює код і адміністрування.
ExecutorService
спрощує керування за рахунок використання пулу потоків, що полегшує обробку завдань. Крім того, ручне створення потоків може призвести до високого споживання ресурсів, тоді як ExecutorService
дозволяє налаштовувати розмір пулу потоків.
Дякуємо за ваш відгук!