Лабораторна робота 6
Робота з потоками даних і потоками виконання у Java
1 Завдання на лабораторну роботу
1.1 Індивідуальне завдання
Необхідно реалізувати індивідуальне завдання попередньої лабораторної роботи, додавши знаходження її коренів (з певною точністю). Алгоритм знаходження коренів полягає в послідовному переборі з певним кроком точок інтервалу, знаходженні інтервалів, на яких функція змінює знак і виведенні середніх арифметичних початків та кінців інтервалів - коренів рівняння. Обчислення здійснювати в окремому потоці виконання.
Користувач повинен мати можливість вводити значення параметрів a і b, діапазон пошуку й точність, та отримувати графік і корені. Реалізувати можливість призупинення, відновлення потоку, а також повного припинення і повторного обчислення з новими даними.
1.2 Обчислення π в окремому потоці виконання (додаткове завдання)
Реалізувати програму обчислення π с точністю до заданого ε як суму послідовності:
Обчислення здійснювати в окремому потоці виконання. Під час виконання обчислення надавати користувачеві можливість уводити запит про кількість обчислених елементів суми.
1.3 Робота з BlockingQueue
Створити консольну програму, в якій один потік виконання додає цілі числа до черги BlockingQueue
,а інший обчислює їх середнє арифметичне.
1.4 Робота з потоками даних
Створити консольну програму, в якій виводяться всі додатні цілі числа, сума цифр яких дорівнює заданому значенню. Використати потоки даних.
1.5 Створення застосунку графічного інтерфейсу користувача для отримання простих множників чисел (додаткове завдання)
За допомогою засобів JavaFX розробити застосунок графічного інтерфейсу користувача, в якому користувач вводить діапазон чисел (від і до), а у вікні відображаються числа і їх прості множники. Реалізувати можливість призупинення, відновлення потоку, а також повного припинення і повторного обчислення з новими даними.
2 Методичні вказівки
2.1 Робота з потоками виконання
2.1.1 Загальні концепції
Процес (process) – це екземпляр комп'ютерної програми, яка завантажена в пам'ять виконується. В сучасних операційних системах процес може виконуватися паралельно з іншими процесами. Процесу виділяється окремий адресний простір, причому цей простір фізично недоступний для інших процесів.
Нитка (thread), або потік виконання (потік управління) - це окрема підзадача, яка може виконуватися паралельно з іншими підзадачами (нитками) в межах одного процесу. Кожен процес містить як мінімум один потік виконання, іменований головним (main thread). Всі потоки виконання, створені процесом, виконуються в адресному просторі цього процесу і мають доступ до ресурсів процесу. Створення потоків виконання – істотно менше ресурсномістка операція, ніж створення нових процесів. Потоки виконання іноді називають легковажними процесами (lightweight processes).
Якщо процес створив кілька потоків, то всі вони виконуються паралельно, при чому час центрального процесора (або декількох центральних процесорів в мультипроцесорних системах) розподіляється між цими потоками. Розподілом часу центрального процесора займається спеціальний модуль операційної системи - планувальник (scheduler). Планувальник по черзі передає управління окремим потокам, так що навіть в однопроцесорній системі створюється ілюзія паралельної роботи запущених потоків. Розподіл часу виконується за перериваннями системного таймера.
Потоки виконання використовують для реалізації незалежних підзадач у межах одного процесу з метою реалізації фонових процесів, моделювання паралельного виконання певних дій або підвищення зручності користувацького інтерфейсу.
2.1.2 Низькорівневі засоби роботи з потокам виконання
Є два підходи до створення об'єкта-нитки з використанням класу Thread
:
- успадкування нового класу від класу
java.lang.Thread
і створення об'єкта нового класу; - створення класу, який реалізує інтерфейс
java.lang.Runnable
; об'єкт такого класу передається конструктору класуjava.lang.Thread
.
Під час створення похідного класу від Thread
необхідно перекрити його метод run()
. Після створення об'єкта-потоку його треба запустити за допомогою методу start()
. Цей метод здійснює певну ініціалізацій роботу і викликає run()
. У наведеному нижче прикладі створюється окремий потік виконання, який здійснює виведення чергового цілого числа від 1 до 40:
package ua.in.iwanoff.java.sixth; public class ThreadTest extends Thread { @Override public void run() { for (int counter = 1; counter <= 40; counter++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } } public static void main(String[] args) { new ThreadTest().start(); // Сюди можна додати дії, що виконуються паралельно з методом run() } }
Виклик методу sleep()
зумовлює припинення потоку на вказану кількість мілісекунд. Виклик методу sleep()
вимагає перехоплення винятку InterruptedException
, який генерується в разі переривання цього потоку виконання іншим потоком. Аналогічний приклад зі створенням об'єкта класу, що реалізує інтерфейс Runnable
. У цьому випадку також необхідно запустити потік за допомогою методу start()
:
package ua.in.iwanoff.java.sixth; public class RunnableTest implements Runnable { @Override public void run() { for (int counter = 1; counter <= 40; counter++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter); } } public static void main(String[] args) { new Thread(new RunnableTest()).start(); // Сюди можна додати дії, що виконуються паралельно з методом run() } }
Другий підхід є кращим, так як в цьому випадку ми вільні у виборі базового класу.
Окремим потокам можна давати імена (другий параметр конструктора) і отримувати ці імена за допомогою функції getName()
.
Будь який потік виконання може знаходитися в декількох стандартних станах. Стан "новий" (Thread.State.NEW
) потік отримує, коли створюється об'єкт потоку. Виклик методу start()
переводить потік зі стану "новий" в стан "працює" (Thread.State.RUNNABLE
). Існують стани "заблокований" (Thread.State.BLOCKED
), "заблокований за часом", або "в режимі очікування" (Thread.State.TIMED_WAITING
), "очікує", або "непрацездатний" (Thread.State.WAITING
) і "завершений" (Thread.State.TERMINATED
). Під час створення потоку він отримує стан "новий" і не виконується. Отримати значення стану потоку можна викликом методу getState()
. Наведений нижче приклад демонструє деякі стани потоку в межах його життєвого циклу:
package ua.in.iwanoff.java.sixth; public class StateTest { public static void main(String[] args) throws InterruptedException { Thread testThread = new Thread(); System.out.println(testThread.getState()); // NEW testThread.start(); System.out.println(testThread.getState()); // RUNNABLE testThread.interrupt(); // перериваємо потік Thread.sleep(100); // потрібен час для завершення потоку System.out.println(testThread.getState()); // TERMINATED } }
Метод wait(long timeout)
, як і метод sleep(long timeout),
дозволяє призупинити роботу потоку на вказану кількість мілісекунд. Під час виконання цього методу також може бути згенеровано виняток InterruptedException
. Застосування цих методів дозволяє перевести потік в режим очікування (TIMED_WAITING
). На відміну від sleep()
, працездатність після виклику методу wait()
можна відновити методами notify()
або notifyAll()
.
Метод wait()
може бути викликаний без параметрів. Потік при цьому переходить в стан "непрацездатний" (WAITING
).
Менш надійним альтернативним способом припинення роботи потоку є виклик методу yield(),
який повинен призводити до припинення потоку на деякий квант часу, для того щоб інші потоки могли виконувати свої дії.
Після завершення виконання методу run()
потік закінчує свою роботу, потік переходить в пасивний стан (TERMINATED
).
У ранніх версіях Java для примусової зупинки, а також для тимчасового призупинення роботи потоку з подальшим відновленням передбачалося використання методів класу Thread
- stop()
, suspend()
і resume()
. Методи stop()
, suspend()
і resume()
вважаються небажаними для використання (deprecated-методи), оскільки вони провокують створення ненадійного коду, який важко зневаджувати. Крім того, використання suspend()
і resume()
може спровокувати взаємні блокування (deadlock).
Перервати потік, який знаходиться в стані очікування (Thread.State.TIMED_WAITING
), можна за допомогою виклику функції interrupt()
. В цьому випадку генерується виняток InterruptedException
. Можна модифікувати попередній приклад так, щоб він дозволяв перервати потік виконання:
package ua.in.iwanoff.java.sixth; import java.util.Scanner; public class InterruptTest implements Runnable { @Override public void run() { for (int counter = 1; counter <= 40; counter++) { try { Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("Потік перерваний."); break; } System.out.println(counter); } } @SuppressWarnings("resource") public static void main(String[] args) { Thread thread = new Thread(new InterruptTest()); thread.start(); System.out.println("Для переривання натисніть Enter"); new Scanner(System.in).nextLine(); thread.interrupt(); } }
Метод join()
дозволяє одному потоку дочекатися завершення іншого. Виклик цього методу всередині потоку t1
для потоку t2
призведе до припинення поточного потоку (t1
) до завершення t2
, як показано в наведеному нижче прикладі:
package ua.in.iwanoff.java.sixth; public class JoinTest { static Thread t1, t2; static class FirstThread implements Runnable { @Override public void run() { try { System.out.println("Потік First запущений."); Thread.sleep(1000); System.out.println("Основна робота потоку First завершена."); t2.join(); System.out.println("Потік First завершено."); } catch (InterruptedException e) { e.printStackTrace(); } } } static class SecondThread implements Runnable { @Override public void run() { try { System.out.println("Потік Second запущений."); Thread.sleep(3000); System.out.println("Потік Second завершено."); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { t1 = new Thread(new FirstThread()); t2 = new Thread(new SecondThread()); t1.start(); t2.start(); } }
Порядок виведення повідомлень буде таким:
Потік First запущений. Потік Second запущений. Основна робота потоку First завершена. Потік Second завершено. Потік First завершено.
Як і sleep()
, join()
реагує на переривання, отже, необхідно перехоплювати виняток InterruptedException
.
Можна створити потік виконання, робота якого переривається при завершенні потоку, який його породив. Для того, щоб створити такий потік, викликається метод setDaemon()
з параметром true
. Метод setDaemon()
необхідно викликати після створення потоку, але до моменту його запуску, тобто перед викликом методу start()
.За допомогою методу isDaemon()
можна перевірити, є потік демоном, чи ні. Якщо потік-демон створює інші потоки, то вони також отримають статус потоку-демона.
package ua.in.iwanoff.java.sixth; public class Th extends Thread { public void run() { try { if (isDaemon()) { System.out.println("старт потоку-демона"); sleep(1000); } else { System.out.println("старт звичайного потоку"); } } catch (InterruptedException e) { System.err.print("Error" + e); } finally { if (!isDaemon()) System.out.println("завершення звичайного потоку"); else System.out.println("завершення потоку-демона"); } } public static void main(String[] args) { Th usual = new Th(); Th daemon = new Th(); daemon.setDaemon(true); daemon.start(); usual.start(); System.out.println("останній рядок main"); } }
Після компіляції і запуску, можливо, буде виведено:
старт потоку-демона останній рядок main старт звичайного потоку завершення звичайного потоку
Потік-демон (через виклик методу sleep(10000)
) не встиг завершити виконання свого коду до завершення основного потоку застосунку, пов'язаного з методом main()
. Базова властивість потоків-демонів полягає в можливості основного потоку застосунку завершити виконання з закінченням коду методу main()
, не звертаючи уваги на те, що потік-демон ще працює. Якщо поставити час затримки також для потоку main()
, то потік-демон може встигнути завершити своє виконання до закінчення роботи основного потоку:
старт потоку-демона
старт звичайного потоку
завершення звичайного потоку
завершення потоку-демона
останній рядок main
Якщо запущено декілька потоків виконання, потрібен спосіб для виключення можливості використання одного ресурсу двома потоками. Якщо елементи даних класу оголошені як private
і доступ до цієї області пам'яті можливий тільки за допомогою методів, то можна уникнути колізій, оголосивши ці методи як synchronized
. Одночасно тільки один потік може викликати synchronized
метод для певного об'єкта. Такий метод або фрагмент коду іменується критичної секцією.
Для забезпечення доступу до такого коду використовується концепція монітора. Під монітором розуміють деякий об'єкт, що забезпечує блокування коду під час виконання його деяким потоком. Якщо ключове слово synchronized
розташоване перед заголовком функції, монітором є об'єкт, для якого викликаний цей метод (this
). Після того, як викликаний хоча б один синхронізований метод, блокування поточним об'єктом поширюється на всі методи з модифікатором synchronized
.
Розглянемо такий приклад. Клас Adder
дозволяє додавати цілі числа до деякого накопичувача. Клас AdderThread
реалізує потік виконання, що забезпечує додавання послідовних п'яти цілих значень. У функції main()
класу AdderTest
створюємо два потоки і запускаємо їх:
package ua.in.iwanoff.java.sixth; class Adder { long sum = 0; public void add(long value) { this.sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); } } class AdderThread extends Thread { private Adder counter = null; public AdderThread(String name, Adder counter) { super(name); this.counter = counter; } public void run() { try { for (int i = 0; i < 5; i++) { counter.add(i); Thread.sleep(10); } } catch (InterruptedException e) { e.printStackTrace(); } } } public class AdderTest { public static void main(String[] args) { Adder adder = new Adder(); Thread threadA = new AdderThread("A", adder); Thread threadB = new AdderThread("B", adder); threadA.start(); threadB.start(); } }
Завантажена декілька разів на виконання програма буде продукувати різні проміжні результати, що пов'язано з некоректною роботою двох потоків з одним об'єктом. Для виправлення ситуації перед методом add()
слід додати модифікатор synchronized
:
synchronized public void add(long value) { this.sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); }
Іноді оголошувати весь метод як synchronized
незручно, оскільки одночасно інші нитки могли б виконувати інші заблоковані методи. Для вирішення даної проблеми використовують блок, що починається зі слова synchronized
і містить після цього слова в дужках ім'я об'єкта, який відповідає за блокування. Монітором може бути будь-який об'єкт, похідний від java.lang.Object
, що підтримує необхідні для цього методи. У наведеному нижче прикладі створюється спеціальний об'єкт для блокування:
public void add(long value) { Object lock = new Object(); synchronized (lock) { this.sum += value; System.out.println(Thread.currentThread().getName() + " " + sum); } }
Синхронізації не вимагають процеси запису і читання об'єктів, розміри яких не перевищують 32 біт. Такі об'єкти називаються атомарними.
Під час обміну даних через поля слід використовувати ключове слово volatile
перед описом відповідного поля. Це пов'язано з наявністю механізму кешування даних окремо для кожного потоку, і значення з цього кешу може відрізнятися для кожного з них. Оголошення поля з ключовим словом volatile
відключає для нього таке кешування:
volatile long sum = 0;
Метод wait()
, викликаний всередині синхронізованого блоку або методу, зупиняє виконання поточного потоку і звільняє від блокування захоплений об'єкт, зокрема об'єкт lock
. Повернути блокування об'єкта потоку можна викликом методу notify()
для конкретного потоку або notifyAll()
для всіх потоків. Виклик може бути здійснений тільки з іншого потоку, який заблокував, в свою чергу, зазначений об'єкт.
За допомогою методу setPriority()
можна змінювати пріоритети потоків. Не рекомендується встановлювати вищий пріоритет (константа Thread.MAX_PRIORITY
).
2.1.3 Використання потоків виконання у JavaFX-застосунках
Робота застосунків графічного інтерфейсу користувача зазвичай пов'язана з потоками виконання. Після завантаження JavaFX-застосунку на виконання автоматично створюється потік застосунку (application thread), в якому здійснюється обробка подій. Тільки цей потік може мати взаємодію з вікнами й візуальними компонентами. Спроба двох потоків одночасно керувати зовнішнім виглядом компонентів та інформацією у вікні може призвести до хаотичного вигляду й непередбачуваної поведінки візуальних компонентів. Разом з тим, саме завдяки багатопотоковості можна забезпечити керованість застосунком. Крім того, доцільно деякі дії, які виконуються тривалий час, запускати в окремому потоці виконання, що забезпечить можливість відслідковувати процес, призупиняти й продовжувати його, змінювати налаштування тощо.
Засоби JavaFX надають можливість виконувати код в потоці виконання, який відповідає за отримання і обробку подій. Метод javafx.application.Platform.runLater()
отримує об'єкт, який реалізує функціональний інтерфейс Runnable
. Цей об'єкт потрапляє у потік обробки подій, де об'єкт стає в чергу і, коли виникає можливість, виконується його метод run()
. Саме в цьому методі доцільно розташувати взаємодію з візуальними компонентами. Приклад 3.6 демонструє можливість виклику цього методу з дочірнього потоку.
Існують також високорівневі механізми роботи з потоками виконання у JavaFX, представлені в пакеті javafx.concurrent
: абстрактний клас Task
, похідний від java.util.concurrent.FutureTask
, в свою черг передбачає створення похідного класу, який реалізує метод call(), в якому реалізована взаємодія з візуальними компонентами. Крім того, цей клас надає можливість безпосереднього зв'язування деяких компонентів (наприклад, ProgressBar
) з задачею (Task
).
2.2 Використання колекцій, безпечних з точки зору багатопотоковості
2.2.1 Використання синхронізованих аналогів стандартних колекцій
Засобами класу java.util.Collections
можна отримати безпечні з точки зору багатопотоковості клони існуючих колекцій. Це такі статичні функції, як synchronizedCollection()
, synchronizedList()
, synchronizedMap()
, synchronizedSet()
, synchronizedSortedMap()
і synchronizedSortedSet()
. Кожна з цих функцій приймає як параметр відповідну несинхронізовану колекцію і повертає колекцію такого ж типу, всі методи якої (крім ітераторів) визначені як synchronized
. Відповідно всі операції, включаючи читання, використовують блокування, що знижує ефективність роботи і обмежує використання відповідних колекцій.
Ітератори стандартних колекцій, реалізованих у java.util
, реалізують так звану fail-fast поведінку: у випадку зміни об'єкта в процесі обходу колекції ітератором генерується виняток ConcurrentModificationException
, причому в умовах багатопотоковості поведінка ітератора може бути непередбачуваною - виняток може бути згенеровано не відразу або не згенеровано взагалі. Надійну роботу з елементами колекції забезпечують спеціальні контейнерні класи, визначені в java.util.concurrent
. Це такі типи, як CopyOnWriteArrayList
, CopyOnWriteArraySet
, ConcurrentMap
(інтерфейс), ConcurrentHashMap
(реалізація), ConcurrentNavigableMap
(интерфейс) і ConcurrentSkipListMap
(реалізація).
Клас CopyOnWriteArrayList
, який реалізує інтерфейс List
, для всіх операцій зміни значення елементів створює копію масиву. Ітератор також працює зі своєю копією. Така поведінка ітератора має назву fail-safe поведінки.
Класи ConcurrentHashMap
і ConcurrentSkipListMap
забезпечують роботу з асоціативними масивами, яка не потребує блокування всіх операцій. Клас ConcurrentHashMap
організовує структуру даних, розділену на сегменти. Блокування здійснюється на рівні одного сегмента. За рахунок поліпшеного хеш-функції ймовірність звернення двох ниток до одного сегмента істотно знижується. Клас ConcurrentSkipListMap
реалізує зберігання з використанням спеціальної структури даних - skip list (Список з пропусками). Елементи асоціативного масиву впорядковані за ключами. У прикладі 2.2 продемонстровано роботу з ConcurrentSkipListMap
.
2.2.2 Робота зі спеціальними колекціями, які використовують блокування
Пакет java.util.concurrent
надає набір колекцій, безпечних з точки зору багатопотоковості. Це такі узагальнені типи, як BlockingQueue
(інтерфейс) і його стандартні реалізації (ArrayBlockingQueue
, LinkedBlockingQueue
, PriorityBlockingQueue
, SynchronousQueue
, DelayQueue
), похідні від нього інтерфейси TransferQueue
(реалізація LinkedTransferQueue
) і LinkedBlockingDeque
(стандартна реалізація – LinkedBlockingDeque
).
Інтерфейс BlockingQueue
представляє чергу, яка дозволяє додавати і вилучати елементи безпечно з точки зору потоків. Типове використання BlockingQueue
– занесення об'єктів одним потоком, а вилучення – іншим.
Потік, який додає об'єкти (перший потік), продовжує додавати їх в чергу, доки не буде досягнута деяка верхня межа припустимої кількості елементів. У цьому випадку під час спроби додати нові об'єкти перший потік блокується, поки потік виконання, який отримує елементи (другий потік) не вилучить з черги принаймні один елемент. Другий потік може вилучати і використовувати об'єкти з черги. Якщо, проте, другий потік спробує отримати об'єкт з порожньої черги, цей потік блокується, поки перший потік не додасть новий об'єкт.
Iнтерфейс BlockingQueue
походить від java.util.Queue
, тому він підтримує методи add()
, remove()
, element()
(з генерацією винятку), а також offer()
, poll()
и peek()
(з поверненням спеціального значення). Крім того, визначені методи, які блокують - put()
(для додавання) і take()
(для вилучення). Є також методи, які використовують блокування з часовим інтервалом – offer()
і poll()
з додатковими параметрами (інтервалом часу і одиницею часу).
До BlockingQueue
не можна додати значення null
. Така спроба призводить до генерації винятку NullPointerException
.
Найбільш популярна реалізація BlockingQueue
– це ArrayBlockingQueue
. Для зберігання посилань на об'єкти в цій реалізації використовують звичайний масив обмеженої довжини. Ця довжина після створення об'єкта не може бути змінена. Всі конструктори класу першим параметром отримують цю довжину масиву. У наведеному нижче прикладі створюються два потоки - виробник (Producer
) і споживач (Consumer
). Виробник додає цілі числа з затримкою між додаваннями, а споживач їх вилучає.
package ua.in.iwanoff.java.sixth; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; class Producer implements Runnable { private BlockingQueue<Integer> queue; int countToAdd; public Producer(BlockingQueue<Integer> queue, int countToAdd) { this.queue = queue; this.countToAdd = countToAdd; } public void run() { // Намагаємося додавати числа: try { for (int i = 1; i <= countToAdd; i++) { queue.put(i); System.out.printf("Added: %d%n", i); Thread.sleep(100); } } catch (InterruptedException e) { System.out.println("Producer interrupted"); } } } class Consumer implements Runnable { private BlockingQueue<Integer> queue; int countToTake; public Consumer(BlockingQueue<Integer> queue, int countToTake) { this.queue = queue; this.countToTake = countToTake; } public void run() { // Вилучаємо числа: try { for (int i = 1; i <= countToTake; i++) { System.out.printf("Taken by customer: %d%n", queue.take()); } } catch (InterruptedException e) { System.out.println("Consumer interrupted"); } } } public class BlockingQueueDemo { public static void main(String[] args) throws InterruptedException { // Створюємо чергу з 10 елементів: BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); // Створюємо і запускаємо два потоки - для запису і читання: Thread producerThread = new Thread(new Producer(queue, 100)); Thread consumerThread = new Thread(new Consumer(queue, 4)); producerThread.start(); consumerThread.start(); // Чекаємо 10 секунд і перериваємо перший потік: Thread.sleep(10000); producerThread.interrupt(); } }
Під час запуску програми видно, що потік-виробник послідовно додає числа в чергу. Перші чотири числа вилучаються потоком-споживачем, причому потік-споживач кожен раз блокується, чекаючи наступне число. Оскільки в черзі може розміститися лише 10 чисел, після того, як потік-споживач припиняє вилучати елементи, черга швидко переповнюється. За десять секунд головний потік припиняє потік-виробник.
Реалізація LinkedBlockingQueue
відрізняється від ArrayBlockingQueue
внутрішнім поданням – у вигляді зв'язного списку. При цьому максимальний розмір вже не суттєвий (усталене значення – Integer.MAX_VALUE
), однак він також може бути заданий в конструкторі.
Клас PriorityBlockingQueue
використовує ті ж правила впорядкування, що й java.util.PriorityQueue
.
Клас SynchronousQueue
представляє чергу, яка може містити лише один елемент. Потік виконання, який додав елемент, блокується, поки інший потік не отримає його з черги. Якщо другий потік не може отримати елемент (черга порожня), цей потік також блокується, поки новий елемент не буде додано першим потоком.
Реалізація DelayQueue
дозволяє вилучати тільки ті елементи, для яких закінчився деякий проміжок часу (затримка). Елементи цієї черги повинні реалізовувати інтерфейс java.util.concurrent.Delayed
. Цей інтерфейс визначає метод getDelay()
, який повертає затримку, яка залишилася для об'єкта. З черги в першу чергу вилучається елемент, для якого час затримки минув раніше інших. Якщо у жодного з елементів не закінчилася затримка, метод poll()
поверне null
.
Інтерфейс BlockingDeque
, похідний від BlockingQueue
, додатково підтримує операції addFirst()
, addLast()
, takeFirst()
і takeLast()
, характерні для черг з двома кінцями. Клас LinkedBlockingDeque
пропонує стандартну реалізацію цього інтерфейсу.
2.3 Робота з контейнерами і потоками даних Java 8
2.3.1 Використання класу Optional
Опціональні значення - це контейнери для значень, які можуть іноді бути порожніми. Традиційно для невизначених величин використовували значення null
. Використання константи null
може бути незручним, оскільки воно може призвести до генерації винятку NullPointerException
, що ускладнює налагодження і супровід програми.
Узагальнений клас Optional
дозволяє зберегти значення посилання на деякий об'єкт, а також перевірити, чи встановлено значення. Наприклад, деякий метод повертає числове значення, але для деяких значень аргументу не може повернути щось певне. Такий метод може повернути об'єкт типу Optional
і це значення потім може бути використано в викликає функції. Припустимо, деяка функція обчислює і повертає зворотну величину і повертає "порожній" об'єкт, якщо аргумент дорівнює 0. У функції main()
здійснюємо обчислення зворотних величин для чисел з масиву:
package ua.in.iwanoff.java.sixth; import java.util.Optional; public class OptionalDemo { static Optional<Double> reciprocal(double x) { Optional<Double> result = Optional.empty(); if (x != 0) { result = Optional.of(1 / x); } return result; } public static void main(String[] args) { double[] arr = { -2, 0, 10 }; Optional<Double> y; for (double x : arr) { System.out.printf("x = %6.3f ", x); y = reciprocal(x); if (y.isPresent()) { System.out.printf("y = %6.3f%n", y.get()); } else { System.out.printf("Значення не може бути розраховане%n"); } } } }
Якщо не здійснювати перевірку на наявність значення (isPresent()
), при спробі виклику функції get()
для "порожнього" значення буде згенеровано виняток java.util.NoSuchElementException
. Його можна перехоплювати замість виклику функції isPresent()
.
У деяких випадках значення null
не повинно зберігатися як можливе допустиме. В цьому випадку для збереження значення слід використовувати ofNullable()
. Наприклад:
Integer k = null; Optional<Integer> opt = Optional.ofNullable(k); System.out.println(opt.isPresent()); // false
Припустимо, якщо описана раніше функція reciprocal()
не повертає значення в разі ділення на нуль, змінній y
слід присвоїти 0. Традиційно в цьому випадку використовують конструкцію if
... else
або умовну операцію:
y = reciprocal(x); double z = y.isPresent() ? y.get() : 0;
Метод orElse()
дозволяє зробити код більш компактним:
double z = reciprocal(x).orElse(0.0);
Крім узагальненого класу Optional
можна також використовувати класи, оптимізовані для примітивних типів - OptionalInt
, OptionalLong
, OptionalBoolean
тощо. Попередній приклад з обчисленням зворотної величини можна було б реалізувати таким чином (з використанням OptionalDouble
):
package ua.in.iwanoff.java.sixth; import java.util.OptionalDouble; public class OptionalDoubleDemo { static OptionalDouble reciprocal(double x) { OptionalDouble result = OptionalDouble.empty(); if (x != 0) { result = OptionalDouble.of(1 / x); } return result; } public static void main(String[] args) { double[] arr = {-2, 0, 10}; OptionalDouble y; for (double x : arr) { System.out.printf("x = %6.3f ", x); y = reciprocal(x); if (y.isPresent()) { System.out.printf("y = %6.3f%n", y.getAsDouble()); } else { System.out.printf("Значення не може бути розраховане%n"); } } } }
Як видно з прикладу, результат слід отримувати за допомогою методу getAsDouble()
замість get()
.
2.3.2 Додаткові можливості роботи зі стандартними контейнерами
Стандартні інтерфейси пакету java.util
доповнені методами, орієнтованими на використання лямбда-виразів і посилань на методи. Для забезпечення сумісності з попередніми версіями Java нові методи інтерфейсів представлені з усталеною реалізацією. Зокрема, інтерфейс Iterable
визначає метод forEach()
, який дозволяє виконати в циклі деякі дії, що не змінюють елементів колекції. Дію можна задати лямбда-виразом або посиланням на метод. Наприклад:
public class ForEachDemo { static int sum = 0; public static void main(String[] args) { Iterable<Integer> numbers = new ArrayList(Arrays.asList(2, 3, 4)); numbers.forEach(n -> sum += n); System.out.println(sum); } }
У наведеному вище прикладі здійснюється сумування елементів колекції. Змінна-сума описана як статичне поле класу, оскільки лямбда-вирази не можуть змінювати локальні змінні.
Інтерфейс Collection
визначає метод removeIf()
, який дозволяє видалити з колекції дані, відповідні деякому правилу-фільтру. У наведеному нижче прикладі з колекції цілих чисел видаляються непарні елементи. Метод forEach()
використовується для виведення елементів колекції в стовпчик:
Collection<Integer> c = new ArrayList(Arrays.asList(2, 4, 11, 8, 12, 3)); c.removeIf(k -> k % 2 != 0); // Решта елементів виводиться в стовпчик: c.forEach(System.out::println);
Інтерфейс List
надає методи replaceAll()
і sort()
. Останній можна використовувати замість аналогічного статичного методу класу Collections
, проте визначення ознаки сортування є обов'язковим:
List<Integer> list = new ArrayList(Arrays.asList(2, 4, 11, 8, 12, 3)); list.replaceAll(k -> k * k); // замінюємо числа їхніми квадратами System.out.println(list); // [4, 16, 121, 64, 144, 9] list.sort(Integer::compare); System.out.println(list); // [4, 9, 16, 64, 121, 144] list.sort((i1, i2) -> Integer.compare(i2, i1)); System.out.println(list); // [144, 121, 64, 16, 9, 4]
Найбільш суттєві зміни торкнулися інтерфейсу Map
. Додані методи, наведені в таблиці:
Метод | Опис |
---|---|
V getOrDefault(Object key, V& defaultValue) |
Повертає значення, або усталене значення, якщо ключ відсутній |
V putIfAbsent(K key, V value) |
Додає пару, якщо ключ відсутній, і повертає значення |
boolean remove(Object key, Object value) |
Видаляє пару, якщо вона присутня |
boolean replace(K key, V oldValue, V newValue) |
Замінює значення на нове, якщо пара присутня |
V replace(K key, V value) |
Замінює значення, якщо ключ є, повертає старе значення |
V compute(K key, BiFunction<?& super K, super V, ? extends V> remappingFunction) |
Викликає функцію для побудови нового значення. Вводиться нова пара, видаляється пара, яка існувала раніше, і повертається нове значення |
V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) |
Якщо присутній вказаний ключ, для створення нового значення викликається задана функція і нове значення замінює колишнє. |
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) |
Повертає значення за ключем. Якщо ключ відсутній, додається нова пара, значення обчислюється за функцією |
V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) |
Якщо ключ відсутній, то вводиться нова пара і повертається значення v . В іншому випадку задана функція повертає нове значення, виходячи з колишнього значення і ключ оновлюється для доступу до цього значення. а потім воно повертається |
void forEach(BiConsumer<? super K, ? super V> action) |
Виконує задану дію (action ) над кожним елементом |
Наведений нижче приклад демонструє використання деяких із зазначених методів:
package ua.in.iwanoff.java.sixth; import java.util.HashMap; import java.util.Map; public class MapDemo { static void print(Integer i, String s) { System.out.printf("%3d %10s %n", i, s); } public static void main(String[] args) { Map<Integer, String> map = new HashMap<>(); map.put(1, "one"); map.put(2, "two"); map.put(7, "seven"); map.forEach(MapDemo::print); // порядкове виведення System.out.println(map.putIfAbsent(7, "eight")); // seven System.out.println(map.putIfAbsent(8, "eight")); // null System.out.println(map.getOrDefault(2, "zero")); // two System.out.println(map.getOrDefault(3, "zero")); // zero map.replaceAll((i, s) -> i > 1 ? s.toUpperCase() : s); System.out.println(map); // {1=one, 2=TWO, 7=SEVEN, 8=EIGHT} map.compute(7, (i, s) -> s.toLowerCase()); System.out.println(map); // {1=one, 2=TWO, 7=seven, 8=EIGHT} map.computeIfAbsent(2, (i) -> i + ""); System.out.println(map); // нічого не змінилося map.computeIfAbsent(4, (i) -> i + ""); System.out.println(map); // {1=one, 2=TWO, 4=4, 7=seven, 8=EIGHT} map.computeIfPresent(5, (i, s) -> s.toLowerCase()); System.out.println(map); // нічого не змінилося map.computeIfPresent(2, (i, s) -> s.toLowerCase()); System.out.println(map); // {1=one, 2=two, 4=4, 7=seven, 8=EIGHT} // Уводиться нова пара: map.merge(9, "nine", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9)); // nine // Текст зшивається з попереднім: map.merge(9, " as well", (value, newValue) -> value.concat(newValue)); System.out.println(map.get(9)); // nine as well } }
2.3.3 Використання Stream API Java 8
Потоки для роботи з колекціями, або потоки елементів, потоки даних (Stream API) призначені для високорівневої обробки даних, що зберігаються в контейнерах. Їх не слід плутати з потоками введення-виведення (input / output streams) і потоками управління (threads).
Потоковий API використовують для пошуку, фільтрації, перетворення, знаходження мінімальних і максимальних значень, а також іншого маніпулювання даними. Важливою перевагою Stream API є можливість надійної і ефективної роботи в багатопотоковому (multithreading) оточенні.
Потоки слід розуміти не як новий вид колекцій, а як канал передачі і обробки даних. Потік даних працює з деяким джерелом даних, наприклад масивом або колекцією. Потік не зберігає даних безпосередньо, а виконує переміщення, фільтрацію, сортування тощо. Дії, що виконуються потоком, не змінюють даних джерела. Наприклад, сортування даних в потоці не змінює їх порядок в джерелі, а створюється окрема результуюча колекція.
Можна створювати послідовні (sequential) і паралельні (parallel) потоки елементів. Паралельні потоки безпечні з точки зору багатопотоковості. З наявного паралельного потоку можна отримати послідовний і навпаки.
Для роботи з потоками в Java 8 реалізований пакет java.util.stream
, що надає набір інтерфейсів і класів, що забезпечують операції над потоками елементів в стилі функціонального програмування. Потік представлений об'єктом, що реалізує інтерфейс java.util.stream.Stream
. У свою чергу, цей інтерфейс успадковує методи узагальненого інтерфейсу java.util.stream.BaseStream
.
Операції над потоками (методи), визначені в інтерфейсах BaseStream
, Stream
, та інших, похідних від BaseStream
, діляться на проміжні та кінцеві. Проміжні операції отримують і генерують потоки даних і служать для створення так званих конвеєрів (pipeline), в яких над послідовністю виконується ряд дій. Кінцеві операції дають остаточний результат і при цьому "споживають" вихідний потік. Це означає, що вихідний потік не може бути використаний повторно і в разі необхідності повинен бути створений заново.
Найбільш істотні методи узагальненого інтерфейсу java.util.stream.BaseStream
наведені в таблиці (S
- тип потоку, E
- тип елемента, R
- тип контейнера):
Метод | Опис | Примітка |
---|---|---|
S parallel() |
повертає паралельний потік даних, отриманий з поточного | проміжна операція |
S sequential() |
повертає послідовний потік даних, отриманий з поточного | проміжна операція |
boolean isParallel() |
повертає true , якщо потік є паралельним, або false , якщо він є послідовним |
кінцева операція |
S unordered() |
повертає невпорядкований потік, отриманий з поточного | проміжна операція |
Iterator<T> iterator() |
повертає посилання на ітератор потоку | кінцева операція |
Spliterator<T> spliterator() |
повертає посилання на ітератор-роздільник потоку | кінцева операція |
Використання ітераторів потоків буде розглянуто нижче.
Інтерфейс Stream
розширює набір методів для роботи з потоками елементів. Це також узагальнений інтерфейс і він придатний для роботи з будь-якими типами-посиланнями. Нижче наведені найбільш вживані методи інтерфейсу Stream
:
Метод | Опис | Примітка |
---|---|---|
void forEach(Consumer<? super T> action) |
виконує код, заданий дією (action ) для кожного елемента потоку |
кінцева операція |
Stream<T> filter(Predicate<? super T> pred) |
повертає потік елементів, що задовольняють предикату | проміжна операція |
Stream<T> sorted() |
повертає потік елементів, розсортованих в природному порядку | проміжна операція |
Stream<T> sorted(Comparator<? super T> comparator) |
повертає потік елементів, розсортованих в зазначеному порядку | проміжна операція |
|
застосовує задану функцію до елементів потоку і повертає новий потік | проміжна операція |
Optional<T> min(Comparator<? super T> comp) |
повертає мінімальне значення з використанням заданого порівняння | кінцева операція |
Optional<T> max(Comparator<? super T> comp) |
повертає максимальне значення з використанням заданого порівняння | кінцева операція |
long count() |
повертає кількість елементів в потоці |
кінцева операція |
Stream<T> distinct() |
повертає потік різних елементів | проміжна операція |
Optional<T> reduce(BinaryOperator<T> accumulator) |
повертає скалярний результат, обчислений за значеннями елементів | кінцева операція |
Object[] toArray() |
створює і повертає масив елементів потоку | кінцева операція |
Існує кілька способів створення потоку. Можна скористатися "фабричними" методами, що були додані до інтерфейсу Collection
(з усталеними реалізаціями) - відповідно stream()
(для послідовної роботи) и parallelStream()
(для багатопотокового роботи):
List<Integer> intList = Arrays.asList(3, 4, 1, 2); Stream<Integer> sequential = intList.stream(); Stream<Integer> parallel = intList.parallelStream();
Можна створити потік з масиву:
Integer[] a = { 1, 2, 3 }; Stream<Integer> fromArray = Arrays.stream(a);
Можна створити джерело даних із зазначеними елементами. Для цього слід скористатися "фабричним" методом of()
:
Stream<Integer> newStream = Stream.of(4, 5, 6);
Потоки елементів можна створити з потоків введення (BufferedReader.lines()
), заповнити випадковими значеннями (Random.ints()
), а також отримати з архівів, бітових наборів тощо.
Проміжні операції характеризуються так званою відкладеною поведінкою (lazy behaviour): вони виконуються не миттєво, а в міру необхідності - коли кінцева операція працює з новим потоком даних. Відкладена поведінка підвищує ефективність роботи з потоками елементів.
Більшість операцій реалізовані так, що дії над окремими елементами не залежать від інших елементів. Такі операції називаються операціями без збереження стану. Інші операції, що вимагають роботи одразу над всіма елементами (наприклад, sorted()
), називаються операціями зі збереженням стану.
З потоку можна отримати масив за допомогою методу toArray()
. У наведеному нижче прикладі створюється потік, а потім виводиться на консоль через створення масиву і приведення до строковому поданням за допомогою статичного методу Arrays.toString()
:
s = Stream.of (1, -2, 3); System.out.println(Arrays.toString(s.toArray())); // [1, -2, 3]
Потоки елементів надають ітератори. Метод iterator()
інтерфейсу Stream
повертає об'єкт, який реалізує інтерфейс java.util.Iterator
. Ітератор можна використовувати в явному вигляді:
s = Stream.of(11, -2, 3); Iterator<Integer> it = s.iterator(); while (it.hasNext()) { System.out.println(it.next()); }
Існує також спеціальний вид ітератора - ітератор-роздільник (реалізований інтерфейсом Spliterator
). Він, зокрема, дозволяє обійти частину елементів.
Потоки забезпечують ітерацію за елементами даних за допомогою методу forEach()
. Параметр функції - стандартний функціональний інтерфейс Consumer
, що визначає метод з одним параметром і типом результату void
. Наприклад:
Stream<Integer> s = Stream.of(4, 5, 6, 1, 2, 3); s.forEach(System.out::println);
Найбільш проста операція з потоками - фільтрація. Проміжна операція filter()
повертає фільтрований потік, приймаючи параметр типу Predicate
. Тип Predicate
- це функціональний інтерфейс, що описує метод з одним параметром і типом результату boolean
. Наприклад, можна відфільтрувати з потоку s
тільки парні числа:
s.filter(k -> k % 2 == 0).forEach(System.out::println);
Попередній приклад ілюструє використання лямбда-виразів під час роботи з потоками, а також невеличкий конвеєр, що включає одну проміжну операцію.
Проміжна операція sorted()
повертає відсортоване представлення потоку. Елементи упорядковано в природному порядку (якщо він визначений). В інших випадках слід реалізовувати інтерфейс Comparator
, наприклад, за допомогою лямбда-виразу:
// Сортуємо за зростанням: Stream<Integer> s = Stream.of(4, 5, 6, 1, 2, 3); s.sorted().forEach(System.out::println); // Сортуємо в порядку зменшення: s = Stream.of(4, 5, 6, 1, 2, 3); s.sorted((k1, k2) -> Integer.compare(k2, k1)).forEach(System.out::println);
Останній приклад показує, що після кожного виклику кінцевої операції потік потрібно створювати знову.
Проміжна операція map()
отримує функціональний інтерфейс, що задає деяку функцію перетворення і формування нового потоку з отриманих перетворених елементів. Наприклад, обчислюємо квадрати чисел:
s = Stream.of(1, 2, 3); s.map(x -> x * x).forEach(System.out::println);
За допомогою методу distinct()
з колекції можна отримати потік, що містить тільки різні елементи. Наприклад:
s = Stream.of(1, 1, -2, 3, 3); System.out.println(Arrays.toString(s.distinct().toArray())); // [1, -2, 3]
Кінцева операція count()
з типом результату long
повертає кількість елементів в потоці:
s = Stream.of(4, 5, 6, 1, 2, 3); System.out.println(s.count()); // 6
Кінцеві операції min()
і max()
повертають об'єкти Optional
з відповідно мінімальним і максимальним значенням. Для порівняння використовується параметр типу Comparator
. Наприклад:
s = Stream.of(11, -2, 3); System.out.println(s.min(Integer::compare).get()); // -2
За допомогою кінцевої операції reduce()
можна обчислити деяке скалярне значення. Операція reduce()
у своїй простій формі здійснює вказану дію з двома операндами, перший з яких - результат виконання дії над попередніми елементами, а другий - поточний елемент. У наведеному нижче прикладі ми знаходимо суму елементів потоку даних:
s = Stream.of(1, 1, -2, 3, 3); Optional<Integer> sum = s.reduce((s1, s2) -> s1 + s2); sum.ifPresent(System.out::println); // 6
Операції min()
, max()
і reduce()
отримують скалярне значення з потоку, тому вони мають назву операцій зведення (reduction operations).
Існують також потоки для роботи з примітивними типами - IntStream
, LongStream
и DoubleStream
.
3 Приклади програм
3.1 Робота з контейнером ConcurrentSkipListMap
У наведеній нижче програмі один потік виконання здійснює заповнення ConcurrentSkipListMap
парами число / список простих множників, а інший потік знаходить в цьому списку прості числа (числа з одним простим співмножником) і заносить їх до списку. Числа перевіряються в заданому діапазоні.
Клас PrimeFactorization
(розкладання на прості множники):
package ua.in.iwanoff.java.sixth; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.concurrent.ConcurrentSkipListMap; public class PrimeFactorization implements Runnable { static volatile Map<Long, List<Long>> table = new ConcurrentSkipListMap<>(); private long from, to; public long getFrom() { return from; } public long getTo() { return to; } public void setRange(long from, long to) { this.from = from; this.to = to; } // Отримання списку дільників заданого числа: private List<Long> factors(long n) { String number = n + "\t"; List<Long> result = new ArrayList<>(); for (long k = 2; k <= n; k++) { while (n % k == 0) { result.add(k); n /= k; } } System.out.println(number + result); return result; } @Override public void run() { try { for (long n = from; n <= to; n++) { table.put(n, factors(n)); Thread.sleep(1); } } catch (InterruptedException e) { e.printStackTrace(); } } @Override public String toString() { String result = table.entrySet().size() + " numbers\n"; for (Map.Entry<?, ?> e : table.entrySet()) { result += e + "\n"; } return result; } }
Клас Primes
(прості числа):
package ua.in.iwanoff.java.sixth; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; public class Primes implements Runnable { PrimeFactorization pf; Set<Long> result; public Primes(PrimeFactorization pf) { this.pf = pf; } @Override public void run() { result = new LinkedHashSet<>(); try { for (long last = pf.getFrom(); last <= pf.getTo(); last++) { List<Long> factors; do { // намагаємося отримати наступний набір чисел: factors = pf.table.get(last); Thread.sleep(1); } while (factors == null); // Знайдено просте число: if (factors.size() == 1) { result.add(last); System.out.println(this); } } } catch (InterruptedException e) { e.printStackTrace(); } } @Override public String toString() { return " Prime numbers: " + result; } }
Клас PrimesTest
:
package ua.in.iwanoff.java.sixth; public class PrimesTest { public static void main(String[] args) throws InterruptedException { PrimeFactorization pf = new PrimeFactorization(); Primes primes = new Primes(pf); pf.setRange(100000L, 100100L); new Thread(pf).start(); Thread primesThread = new Thread(primes); primesThread.start(); primesThread.join(); System.out.println(pf); System.out.println(primes); } }
3.2 Отримання таблиці простих чисел за допомогою потоків даних
Наведена нижче програма дозволяє отримати таблицю простих чисел в заданому діапазоні. Для отримання простих чисел доцільно використовувати потік IntStream
:
package ua.in.iwanoff.java.sixth; import java.util.stream.IntStream; public class PrimeFinder { private static boolean isPrime(int n) { return n > 1 && IntStream.range(2, n - 1).noneMatch(k -> n % k == 0); } public static void printAllPrimes(int from, int to) { IntStream primes = IntStream.range(from, to + 1).filter(PrimeFinder::isPrime); primes.forEach(System.out::println); } public static void main(String[] args) { printAllPrimes(6, 199); } }
Метод isPrime()
перевіряє, чи є число n простим. Для цього для чисел, більших 1, формується набір послідовних цілих чисел, для кожного з яких перевіряється, чи ділиться n на це число. У методі printAllPrimes()
формуємо потік простих цілих чисел з використанням фільтру і виводимо числа за допомогою методу forEach()
.
3.3 Створення застосунку графічного інтерфейсу користувача для обчислення і відображення простих чисел
Припустимо, необхідно розробити програму отримання простих чисел у діапазоні від 1 до певного значення, яке може бути досить великим. Для знаходження простих чисел будемо застосовувати найпростіший алгоритм послідовної перевірки усіх чисел від 2 до квадратного кореня з числа, що перевіряється. Така перевірка може зайняти багато часу. Для створення зручного інтерфейсу користувача доцільно використовувати окремий потік виконання, у якому здійснюється перевірка. Завдяки цьому можна буде призупиняти та відновлювати пошук, виконувати стандартні маніпуляції з вікном, включаючи його закриття до завершення процесу пошуку.
Кореневою панеллю нашого застосунку буде BorderPane
. Сирцевий код JavaFX-застосунку буде таким:
package ua.in.iwanoff.java.sixth; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.layout.BorderPane; import javafx.stage.Stage; import java.io.FileInputStream; import java.io.IOException; public class PrimeApp extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { FXMLLoader loader = new FXMLLoader(); try { BorderPane root = loader.load(new FileInputStream("PrimeAppWindow.fxml")); Scene scene = new Scene(root, 700, 500); primaryStage.setScene(scene); primaryStage.setTitle("Прості числа"); primaryStage.show(); } catch (IOException e) { e.printStackTrace(); } } }
Інтерфейс користувача включатиме верхню панель (HBox
), до якої послідовно додано мітку з текстом "До:", поле введення тексту textFieldTo
, а також чотири кнопки buttonStart
, buttonSuspend
, buttonResume
і buttonStop
з текстом відповідно "Стартувати", "Призупинити","Продовжити" і "Завершити". На початку виконання програми лише кнопка buttonStart
буде доступною. Середню частину займатиме область введення textAreaResults
, для якої вимкнене редагування, нижню частину займатиме компонент progressBar
, у якому відображатиметься доля виконання процесу пошуку простих чисел. Файл FXML-документу буде таким:
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.geometry.Insets?> <?import javafx.scene.control.Button?> <?import javafx.scene.control.Label?> <?import javafx.scene.control.TextArea?> <?import javafx.scene.control.TextField?> <?import javafx.scene.layout.BorderPane?> <?import javafx.scene.layout.HBox?> <?import javafx.scene.control.ProgressBar?> <BorderPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ua.in.iwanoff.java.sixth.PrimeAppController"> <top> <HBox prefHeight="3.0" prefWidth="600.0" spacing="10.0" BorderPane.alignment="CENTER"> <padding> <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/> </padding> <Label text="До:"/> <TextField fx:id="textFieldTo" text="1000"/> <Button fx:id="buttonStart" minWidth="80.0" text="Стартувати" onAction="#startClick"/> <Button fx:id="buttonSuspend" minWidth="80.0" disable="true" text="Призупинити" onAction="#suspendClick"/> <Button fx:id="buttonResume" minWidth="80.0" disable="true" text="Продовжити" onAction="#resumeClick"/> <Button fx:id="buttonStop" minWidth="80.0" disable="true" text="Завершити" onAction="#stopClick"/> </HBox> </top> <center> <TextArea fx:id="textAreaResults" editable="false" wrapText="true" BorderPane.alignment="CENTER"/> </center> <bottom> <ProgressBar fx:id="progressBar" maxWidth="Infinity" progress="0"/> </bottom> </BorderPane>
Можна згенерувати порожній клас контролера, додати до нього поля, пов'язані з візуальними компонентами й методи обробки подій:
package ua.in.iwanoff.java.sixth; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; public class PrimeAppController { @FXML private TextField textFieldTo; @FXML private Button buttonStart; @FXML private Button buttonSuspend; @FXML private Button buttonResume; @FXML private Button buttonStop; @FXML private ProgressBar progressBar; @FXML private TextArea textAreaResults; @FXML private void startClick(ActionEvent actionEvent) { } @FXML private void suspendClick(ActionEvent actionEvent) { } @FXML private void resumeClick(ActionEvent actionEvent) { } @FXML private void stopClick(ActionEvent actionEvent) { } }
Після завантаження на виконання вікно програми буде таким:
Тепер окремо від засобів графічного інтерфейсу користувача можна створити клас, який відповідатиме за обчислення простих чисел в окремому потоці. Клас PrimeNumbers
реалізуватиме інтерфейс Runnable
. Основні "робочі" функції (start()
, suspend()
, resume()
і stop()
), які викликатимуться з контролера, змінюють стан об'єкта (поля suspended
і stopped
). Для забезпечення гнучкості програмі здійснюється оновлення даних про прості числа і відсоток виконання процесу через механізм зворотного виклику - передбачені поля класу типу інтерфейсу Runnable
. Для того, щоб виконання методів run()
здійснювалося б в іншому потоці, ці функції викликаються за допомогою Platform.runLater()
. Сирцевий код класу PrimeNumbers
матиме такий вигляд:
package ua.in.iwanoff.java.sixth; import javafx.application.Platform; public class PrimeNumbers implements Runnable { private Thread primesThread; // нитка обчислення простих чисел private int to; // кінець діапазону обчислення простих чисел private int lastFound; // останнє знайдене просте число private Runnable displayFunc; // функція, яка викликається для виведення знайденого числа private Runnable percentageFunc; // функція, яка оновлює кількість відсотків виконаного процесу private Runnable finishFunc; // функція, яка викликається після закінчення private double percentage; private boolean suspended; private boolean stopped; public PrimeNumbers(Runnable addFunc, Runnable percentageFunc, Runnable finishFunc) { this.displayFunc = addFunc; this.percentageFunc = percentageFunc; this.finishFunc = finishFunc; } public int getTo() { return to; } public void setTo(int to) { this.to = to; } public synchronized int getLastFound() { return lastFound; } private synchronized void setLastFound(int lastFound) { this.lastFound = lastFound; } public synchronized double getPercentage() { return percentage; } private synchronized void setPercentage(double percentage) { this.percentage = percentage; } public synchronized boolean isSuspended() { return suspended; } private synchronized void setSuspended(boolean suspended) { this.suspended = suspended; } public synchronized boolean isStopped() { return stopped; } private synchronized void setStopped(boolean stopped) { this.stopped = stopped; } @Override public void run() { for (int n = 2; n <= to; n++) { try { setPercentage(n * 1.0 / to); // Оновлюємо кількість відсотків: if (percentageFunc != null) { Platform.runLater(percentageFunc); } boolean prime = true; for (int i = 2; i * i <= n; i++) { if (n % i == 0) { prime = false; break; } } Thread.sleep(20); if (prime) { setLastFound(n); // Відображаємо знайдене просте число: if (displayFunc != null) { displayFunc.run(); } } } catch (InterruptedException e) { // залежно від стану об'єкта чекаємо на продовження або завершуємо пошук: while (isSuspended()) { try { Thread.sleep(100); } catch (InterruptedException e1) { // Перервали у стані очікування: if (isStopped()) { break; } } } if (isStopped()) { break; } } } if (finishFunc != null) { Platform.runLater(finishFunc); } } public void start() { primesThread = new Thread(this); setSuspended(false); setStopped(false); primesThread.start(); } public void suspend() { setSuspended(true); primesThread.interrupt(); } public void resume() { setSuspended(false); primesThread.interrupt(); } public void stop() { setStopped(true); primesThread.interrupt(); } }
Як видно з коду, методи доступу до даних реалізовані з модифікатором synchronized
, що унеможливлює одночасний запис і читання недописаних даних з різних потоків. Тому всі звертання до полів класу з його методів здійснюються тільки через функції доступу.
Тепер можна реалізувати код контролера:
package ua.in.iwanoff.java.sixth; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; public class PrimeAppController { @FXML private TextField textFieldTo; @FXML private Button buttonStart; @FXML private Button buttonSuspend; @FXML private Button buttonResume; @FXML private Button buttonStop; @FXML private ProgressBar progressBar; @FXML private TextArea textAreaResults; private PrimeNumbers primeNumbers = new PrimeNumbers(this::addToTextArea, this::setProgress, this::finish); @FXML private void startClick(ActionEvent actionEvent) { try { primeNumbers.setTo(Integer.parseInt(textFieldTo.getText())); textAreaResults.setText(""); progressBar.setProgress(0); buttonStart.setDisable(true); buttonSuspend.setDisable(false); buttonResume.setDisable(true); buttonStop.setDisable(false); primeNumbers.start(); } catch (NumberFormatException e) { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("Помилка"); alert.setHeaderText("Хибний діапазон!"); alert.showAndWait(); } } @FXML private void suspendClick(ActionEvent actionEvent) { primeNumbers.suspend(); buttonStart.setDisable(true); buttonSuspend.setDisable(true); buttonResume.setDisable(false); buttonStop.setDisable(false); } @FXML private void resumeClick(ActionEvent actionEvent) { primeNumbers.resume(); buttonStart.setDisable(true); buttonSuspend.setDisable(false); buttonResume.setDisable(true); buttonStop.setDisable(false); } @FXML private void stopClick(ActionEvent actionEvent) { primeNumbers.stop(); } private void addToTextArea() { textAreaResults.setText(textAreaResults.getText() + primeNumbers.getLastFound() + " "); } private void setProgress() { progressBar.setProgress(primeNumbers.getPercentage()); } private void finish() { buttonStart.setDisable(false); buttonSuspend.setDisable(true); buttonResume.setDisable(true); buttonStop.setDisable(true); } }
Методи addToTextArea()
, setProgress()
і finish()
призначені не для безпосереднього виклику з контролера, а для зворотного виклику. Посилання на ці функції передаються в конструктор об'єкта типу PrimeNumbers
. Оброблювачі подій викликають відповідні методи класу PrimeNumbers
і змінюють доступність кнопок.
4 Вправи для контролю
- Створити функцію, яка обчислює квадратний корінь, якщо це можливо, і повертає об'єкт типу
OptionalDouble
. - Створити консольну програму, в якій виводяться всі додатні цілі числа, сума яких дорівнює заданому значенню. Використати потоки даних.
5 Контрольні запитання
- Дайте визначення процесу і потоку виконання.
- У чому переваги використання потоків виконання?
- Які є способи створення потоків виконання в Java?
- В яких станах може перебувати потік?
- Коли здійснюється припинення і призупинення виконання роботи потоку?
- Якими засобами можна здійснити синхронізацію потоків?
- У яких випадках робота потоку блокується?
- Для чого використовується модифікатор
synchronized
? - Які контейнерні класи забезпечують безпеку з точки зору потоків?
- Для чого використовують клас Optional?
- Чи можна використовувати клас Optional з примітивними типами?
- Які додаткові можливості роботи зі стандартними контейнерами передбачені в Java 8?
- У чому переваги і особливості Stream API?
- Як отримати потік з колекції?
- Як отримати потік з масиву?
- Чим відрізняються проміжні і кінцеві операції?