Багатопотокове програмування в java 8

Багатопотокове програмування в java 8
Багатопотокове програмування в java 8

Ласкаво просимо в першу частину керівництва по паралельному програмуванню в Java 8. У цій частині ми на простих прикладах розглянемо, як виконувати код паралельно з допомогою потоків, завдань і сервісів виконавців.

Вперше Concurrency API був представлений разом з виходом Java 5 і з тих пір постійно розвивався з кожною новою версією Java. Більшу частину прикладів можна реалізувати на старіших версіях, проте в цій статті я збираюся використовувати лямбда-вирази. Якщо ви все ще не знайомі з нововведеннями Java 8, рекомендую подивитися моє керівництво.

Потоки і завдання

Всі сучасні операційні системи підтримують паралельне виконання коду за допомогою процесів і потоків. Процес - це екземпляр програми, який запускається незалежно від інших. Наприклад, коли ви запускаєте програму на Java, ОС створює новий процес, який працює паралельно іншим. Усередині процесів ми можемо використовувати потоки, тим самим витиснувши з процесора максимум можливостей.

Потоки (threads) в Java підтримуються починаючи з JDK 1.0. Перш ніж запустити потік, йому треба надати ділянку коду, який зазвичай називається «завданням» (task). Це робиться через реалізацію інтерфейсу Runnable. у якого є тільки один метод без аргументів, який повертає void - run (). Ось приклад того, як це працює:

Коли ви запустите цей код, ви побачите секундну затримку між висновком першої і другої рядки на екран. TimeUnit - корисний клас для роботи з одиницями часу, але те ж саме можна зробити за допомогою Thread.sleep (1000).

Давайте тепер докладніше розглянемо одну з найважливіших частин Concurrency API - сервіс виконавців (executor services).

виконавці

Concurrency API вводить поняття сервісу-виконавця (ExecutorService) - високорівневу заміну роботі з потоками безпосередньо. Виконавці виконують завдання асинхронно і зазвичай використовують пул потоків, так що нам не треба створювати їх вручну. Всі потоки з пулу будуть використані повторно після виконання завдання, а значить, ми можемо створити в додатку стільки завдань, скільки хочемо, використовуючи один виконавець.

Ось як буде виглядати наш перший приклад з використанням виконавця:

ExecutorService executor = Executors. newSingleThreadExecutor ();

executor. submit (() ->

String threadName = Thread. currentThread (). getName ();

System. out. println ( "Hello" + threadName);

// => Hello pool-1-thread-1

Клас Executors надає зручні методи-фабрики для створення різних сервісів виконавців. В даному випадку ми використовували виконавець з одним потоком.

Результат виглядає так само, як в минулий раз. Але у цього коду є важлива відмінність - він ніколи не зупиниться. Роботу виконавців треба завершувати явно. Для цього в інтерфейсі ExecutorService є два методи: shutdown (). який чекає завершення запущених завдань, і shutdownNow (). який зупиняє виконавець негайно.

Ось як я вважаю за краще зупиняти виконавців:

В наведеному вище прикладі використаний ще один вид виконавців, який створюється за допомогою методу newWorkStealingPool (). Цей метод з'явився в Java 8 і поводиться не так, як інші: замість використання фіксованої кількості потоків він створює ForkJoinPool з певним паралелізмом (parallelism size). за замовчуванням рівною кількості ядер машини.

ForkJoinPool вперше з'явився в Java 7, і ми розглянемо його докладніше в наступних частинах нашого керівництва. А тепер давайте подивимося на виконавці з планувальником (scheduled executors).

Виконавці з планувальником

Ми вже знаємо, як віддати завдання виконавцю і отримати її результат. Для того, щоб періодично запускати завдання, ми можемо використовувати пул потоків з планувальником.

ScheduledExecutorService здатний запускати завдання один або кілька разів через встановлені проміжки часу.

Цей приклад показує, як змусити виконавець виконати завдання через три секунди: