Використання віртуальних потоків у Java 21 для високопродуктивної паралельності

текст перекладу

pic

У сучасній розробці на Java ефективність і масштабованість є ключовими питаннями, особливо коли йдеться про I/O-зв'язані задачі, які вимагають очікування відповідей від зовнішніх систем (наприклад, бази даних, API або інших сервісів). Завдяки впровадженню віртуальних потоків у Java 21 підхід до паралельного виконання задач зазнав змін, надаючи розробникам більш ефективний спосіб обробляти такі задачі.

У цій статті ми порівняємо традиційну модель паралельного виконання на основі пулу потоків (наприклад, CompletableFuture з пулом потоків) і новий підхід віртуальних потоків, доступний у Java 21. Також ми розглянемо, як обробляти кілька асинхронних задач за допомогою обох підходів у контексті системи управління інвентарем книг.

Розуміння паралельного виконання в Java 17 та раніше

У версіях до Java 21 (наприклад, Java 17) зазвичай використовувався CompletableFuture з кастомізованим пулом потоків для обробки кількох асинхронних задач. Типовий приклад включає використання пулу потоків для одночасного виконання задач. Ось приклад, де дві задачі виконуються асинхронно, кожна з яких отримує дані з різних джерел у системі управління інвентарем книг:

Використання CompletableFuture з пулом потоків (Java 17 та раніше)

 // Оголошуємо CompletableFuture для отримання інвентарю книг з бази даних  
CompletableFuture<List<Book>> booksFromDBFuture = CompletableFuture.supplyAsync(() -> {  
 try {  
 return getBooksFromDatabase(inventoryRequest);  
 } catch (InventoryException e) {  
 throw new InventoryException(e.getMessage());  
 }  
}, threadPoolExecutor);  

// Оголошуємо ще один CompletableFuture для отримання книг з зовнішнього API  
CompletableFuture<List<Book>> booksFromApiFuture = CompletableFuture.supplyAsync(() -> {  
 try {  
 return getBooksFromExternalApi(inventoryRequest);  
 } catch (InventoryException e) {  
 throw new InventoryException(e.getMessage());  
 }  
}, threadPoolExecutor);  

// Чекаємо на завершення обох задач  
List<Book> booksFromDB = booksFromDBFuture.join();  
List<Book> booksFromApi = booksFromApiFuture.join();

У цьому коді:

  • Ми створюємо два об'єкти CompletableFuture для обробки асинхронних запитів до бази даних та зовнішнього API для отримання книг.
  • Ці задачі відправляються на виконання до кастомізованого пулу потоків (threadPoolExecutor).
  • Метод join() використовується для блокування основного потоку до завершення обох задач.

Хоча цей підхід працює, у нього є певні обмеження:

  • Навантаження на пул потоків: Управління та підтримка фіксованого пулу потоків може бути неефективним, особливо коли є велика кількість короткоживучих задач.
  • Проблеми масштабованості: Обробка великого обсягу паралельних задач за допомогою традиційних потоків може призвести до значного навантаження на пам'ять.

Віртуальні потоки в Java 21

Java 21 привнесла значні покращення у вигляді віртуальних потоків. Віртуальні потоки — це легковажні потоки, які управляються Java Virtual Machine (JVM), дозволяючи масштабуватися з набагато меншою накладною вартістю порівняно з традиційними потоками. Віртуальні потоки ідеально підходять для I/O-зв'язаних задач, таких як запити до бази даних та API, тому що вони дозволяють додатку обробляти тисячі або навіть мільйони паралельних задач без обмежень традиційних пулів потоків.

Використання віртуальних потоків з Executor у Java 21

Java 21 дозволяє створювати віртуальні потоки за допомогою Executors.newVirtualThreadPerTaskExecutor().
текст перекладу

Цей метод створює новий віртуальний потік для кожної задачі без необхідності управляти традиційним пулом потоків.

Ось як ви можете рефакторити попередній приклад, використовуючи віртуальні потоки для управління задачами інвентаризації книг:

// Створення виконуваного віртуального потоку для обробки задач одночасно  
Executor virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();  

// Оголошуємо CompletableFuture для отримання інвентаризації книг з бази даних за допомогою віртуального потоку  
CompletableFuture<List<Book>> booksFromDBFuture = CompletableFuture.supplyAsync(() -> {  
 try {  
 return getBooksFromDatabase(inventoryRequest);  
 } catch (InventoryException e) {  
 throw new InventoryException(e.getMessage());  
 }  
}, virtualThreadExecutor);  

// Оголошуємо ще один CompletableFuture для отримання книг з зовнішнього API за допомогою віртуального потоку  
CompletableFuture<List<Book>> booksFromApiFuture = CompletableFuture.supplyAsync(() -> {  
 try {  
 return getBooksFromExternalApi(inventoryRequest);  
 } catch (InventoryException e) {  
 throw new InventoryException(e.getMessage());  
 }  
}, virtualThreadExecutor);  

// Чекаємо на завершення обох задач і збираємо результати  
List<Book> booksFromDB = booksFromDBFuture.join();  
List<Book> booksFromApi = booksFromApiFuture.join();

Чому варто використовувати віртуальні потоки?

Віртуальні потоки мають кілька ключових переваг порівняно з традиційними потоками в пулі потоків:

  1. Мінімальні накладні витрати: Віртуальні потоки — це легковажні потоки, які мають набагато менші накладні витрати на пам'ять і процесор у порівнянні з традиційними потоками. Це дозволяє масштабувати ваш додаток для обробки набагато більшої кількості паралельних задач.
  2. Спрощена паралельність: На відміну від традиційних пулів потоків, де потрібно керувати фіксованим пулом потоків, віртуальні потоки дозволяють кожній задачі виконуватись на окремому потоці, що робить код простішим і легшим для розуміння.
  3. Краща масштабованість для I/O: Віртуальні потоки особливо підходять для I/O-зв'язаних задач, таких як запити до баз даних або виклики API, оскільки JVM може призупинити потік під час очікування завершення I/O операцій і відновити його, коли операція завершиться.

Покращення продуктивності з віртуальними потоками

Віртуальні потоки особливо корисні в ситуаціях, коли потрібно обробляти велику кількість паралельних I/O-зв'язаних задач. Наприклад, якщо у вас є система управління інвентарем, яка здійснює багато викликів до бази даних або API для отримання деталей книг, віртуальні потоки дозволяють ефективно управляти тисячами або навіть мільйонами паралельних задач без значного впливу на продуктивність.

Використовуючи Executors.newVirtualThreadPerTaskExecutor(), система може ефективно планувати та управляти задачами без необхідності в традиційному пулі потоків. JVM керує плануванням віртуальних потоків, а потоки операційної системи використовуються повторно за потреби, що призводить до більш ефективного використання ресурсів.

Порівняння: Віртуальні потоки vs Традиційний пул потоків

  • Традиційний пул потоків:
    • Обмежений розміром пулу.
    • Накладні витрати на створення та управління потоками.
    • Може виникати конкуренція потоків при великій кількості задач.
  • Віртуальні потоки:
    • Не потрібно вручну управляти пулом потоків.
    • Практично відсутні накладні витрати на потік.
    • Може ефективно обробляти мільйони паралельних задач.

Висновок

З впровадженням віртуальних потоків у Java 21 управління паралельністю стало набагато простішим і ефективнішим. Використовуючи виконавців для віртуальних потоків, розробники тепер можуть масштабувати свої додатки з мінімальними накладними витратами і без складнощів управління традиційними пулами потоків.

Для додатків з великими навантаженнями I/O, таких як системи управління інвентарем книг, які взаємодіють з базами даних або зовнішніми API, віртуальні потоки стають справжнім проривом, дозволяючи розробникам легко обробляти величезну кількість паралельних задач. Це робить віртуальні потоки відмінним вибором для високопродуктивних, масштабованих Java додатків.

Перекладено з: Leveraging Virtual Threads in Java 21 for High-Performance Concurrency

Leave a Reply

Your email address will not be published. Required fields are marked *