en

Завдання для самостійної роботи

Метапрограмування. Високорівневі засоби роботи з потоками виконання

1 Завдання на роботу

1.1 Побудова графіків довільних функцій

Створити програму графічного інтерфейсу користувача, яка призначена для побудови графіку довільних функцій. Користувач повинен увести дійсні значення a і b, функції f(x) і g(x) у вигляді рядків, які відповідають синтаксису Java. У програмі здійснюється обчислення функції h(x) відповідно до індивідуального завдання:

Номери варіантів
Функція h(x)
Номери варіантів
Функція h(x)
1
13
af(x) – b∙g(x)
7
19
f(a + x) + b∙g(x)
2
14
f(x + a) + g(x – b)
8
20
f(a / x) – g(b∙x)
3
15
(a-f(x))(b + g(x))
9
21
f(x – a) ∙g(x + b)
4
16
f(a∙x) – g(b∙x)
10
22
f(a / x) + g(b / x)
5
17
f(x / a)∙ g(x + b)
11
23
a∙f(x) + b∙g(x)
6
18
f(a / x) – g(b / x)
12
24
af(x)g(b∙x)

Після введення необхідних функцій, діапазону відображення графіку і натиснення відповідної кнопки здійснюється побудова графіку. Слід також передбачити функцію очищення рядків уведення й графіку.

Для програмної реалізації обробки введених виразів слід застосувати динамічну компіляцію коду. Для створення застосунку графічного інтерфейсу користувача слід використати засоби JavaFX. Рекомендований підхід - використання компонента LineChart.

1.2 Інтерпретація математичних виразів

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

Примітка. Синтаксис математичних виразів JavaScript аналогічний Java. Результат можна виводити за допомогою функції print() без створення додаткових змінних.

1.3 Робота з BlockingQueue

Створити консольну програму, в якій один потік виконання додає цілі числа до черги BlockingQueue,а інший обчислює їх середнє арифметичне.

2 Методичні вказівки

2.1 Метапрограмування

2.1.1 Загальні концепції. Засоби виконання сценаріїв

Метапрограмування – це процес створення програм, які породжують інші програми. Найважливішим елементом метапрограмування є автоматична генерація коду. Можна запропонувати два підходи – з використанням скриптових мов і з використанням компіляції "на льоту".

Багато мов сценаріїв і динамічно типізованих мов дозволяють генерувати байт-код Java, так що програми можуть виконуватися на платформі Java, так само як програми Java. Засоби пакета javax.script, доданого в Java SE 6, дозволяють інтерпретувати вирази, написані на скриптових мовах (AppleScript, Groovy, JavaScript, Jelly, PHP, Python, Ruby тощо).

Для обробки скриптів необхідно завантажити "машину скриптів" (script engine). У Java 8 машина (движок) обробки скриптів, зокрема, мовою JavaScript (Nashorn JavaScript Engine), входила у склад JDK. У новітніх версіях Java засоби "машину" обробки скриптів слід завантажувати окремо. Замість Nashorn JavaScript Engine слід застосовувати засоби GraalVM JavaScript. До файлу pom.xml у Maven-проєкті слід додати такі залежності:

<dependency>
    <groupId>org.graalvm.js</groupId>
    <artifactId>js</artifactId>
    <version>24.1.0</version>
</dependency>
<dependency>
    <groupId>org.graalvm.js</groupId>
    <artifactId>js-scriptengine</artifactId>
    <version>24.1.0</version>
</dependency>

Тепер можна інтерпретувати код, написаний на JavaScript:

package ua.inf.iwanoff.java.advanced.additional;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class EvalScript {

    static void main() throws Exception {
        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName("graal.js");
        engine.eval("print('Hello, World')");
    }
}

Як видно з прикладу, клас ScriptEngineManager дозволяє створити движок JavaScript, а екземпляр класу ScriptEngine здійснює інтерпретацію виразів.

Виконання прикладу призведе до виникнення попереджень, пов'язаних з необхідністю конфігурування журналу та низькою продуктивністю через використання інтерпретації. Але скрипт буде виконано. Для того, щоб частково прибрати непотрібні попередження, замість стандартних засобів роботи з машиною скриптів можна скористатися класами, яки надає GraalVM JavaScript. Результат, аналогічний попередньому, але без зайвих попереджень, можна отримати, створивши об'єкт типу org.graalvm.polyglot.Context:

package ua.inf.iwanoff.java.advanced.additional;

import org.graalvm.polyglot.Context;

public class EvalScript {

    static void main() throws Exception {
        try (Context ctx = Context.newBuilder("js")
                .option("engine.WarnInterpreterOnly", "false")
                .build()) {
            ctx.eval("js", "print('Hello, World');");
        }
    }
}

Вираз мовою JavaScript, або його частина, може бути прочитано з потоку введення, наприклад, з клавіатури. Клас ScriptEngine також дозволяє створювати змінні скриптової мови (метод put()), викликати її функції (метод invokeFunction()) тощо.

Для передачі інформації з Java в скрипт використовується інтерфейс зв'язування Bindings:

package ua.inf.iwanoff.java.advanced.additional;

import javax.script.Bindings;

import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class BindingDemo {

    static void main() throws Exception {
        ScriptEngineManager factory = new ScriptEngineManager();
        ScriptEngine engine = factory.getEngineByName("graal.js");
        engine.put("a", 1);
        engine.put("b", 5);

        Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
        Object a = bindings.get("a");
        Object b = bindings.get("b");
        System.out.println("a = " + a);
        System.out.println("b = " + b);

        Object result = engine.eval("a + b;");
        System.out.println("a + b = " + result);
    }
}

2.1.2 Динамічна генерація коду

Однією з можливостей Java є виклик Java-компілятора у вихідному коді. З цією метою використовують пакет javax.tools. Клас javax.tools.JavaCompiler забезпечує компіляцію зазначеного вихідного коду в файл .class. Створити екземпляр JavaCompiler можна за допомогою фабричного методу getSystemJavaCompiler() класу javax.tools.ToolProvider:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

Припустимо, в каталозі c:\java є підкаталог (пакет) test, а в ньому – файл Test.java:

package test;

public class Test {

    static void main() {
        System.out.println("Hello, Dynamic Compiling!");
    }
}

За допомогою виклику функції run() об'єкта compiler можна здійснити компіляцію вихідного коду. Перші три параметри функції run() – відповідно потоки введення, виведення даних і виведення помилок. Ці потоки використовуються у процесі компіляції. Якщо значення параметрів встановити null, будуть використовуватися відповідно System.in, System.out і System.err.У четвертому і подальших параметрах вказуються аргументи й опції компіляції. У найпростішому випадку необхідно тільки вказати повне ім'я файлу з вихідним кодом. Наприклад:

compiler.run(null, null, null, "c:/java/test/Test.java");

Тепер скомпільовану програму можна завантажити на виконання. Для того, щоб завантажити клас, не зазначений у змінній CLASSPATH, необхідно використовувати спеціальний завантажувач класів – java.net.URLClassLoader. Параметр конструктора цього класу – масив об'єктів класу URL. Клас URL представляє уніфікований визначник розташування ресурсу (Uniform Resource Locator), вказівник на "ресурс" у World Wide Web. Ресурс може бути чимось простим, типу файлу або каталогу, або це може бути посилання на більш складний об'єкт, такий як запит до бази даних або до пошукової системи. У нашому випадку такий масив буде складатися з одного елемента. Отримати такий елемент можна з об'єкта типу java.net.URI (уніфікований ідентифікатор ресурсів, Uniform Resource Identifier), який, своєю чергою, може бути отриманий з об'єкта java.io.File:

URLClassLoader classLoader = new URLClassLoader(
    new URL[] { new File("c:/java").toURI().toURL() });

Отриманий об'єкт classLoader можна використовувати для завантаження класу:

Class<?> cls = Class.forName("test.Test", true, classLoader);

Викликаємо метод main():

Method m = cls.getMethod("main", String[].class);
m.invoke(null, new Object[] { new String[] { } });

Примітка: як видно з прикладу, для того, щоб передати параметр-масив, треба зробити інший масив типу Object з масивів.

Весь код, який включає компіляцію та виконання скомпільованої програми, буде таким:

package ua.inf.iwanoff.java.advanced.additional;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class CompileDemo {
    static void main() throws Exception {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        try (URLClassLoader classLoader = new URLClassLoader(
                new URL[]{new File("c:/java").toURI().toURL()})) {
            compiler.run(null, null, null, "c:/java/test/Test.java");
            Class<?> cls = Class.forName("test.Test", true, classLoader);
            Method m = cls.getMethod("main", String[].class);
            m.invoke(null, new Object[]{new String[]{ }});
        }
    }
}

У складнішому випадку крім класу JavaCompiler необхідно залучати інші засоби пакета javax.tools. Зокрема:

  • JavaCompiler.CompilationTask – клас, який представляє завдання компіляції; виконання компіляції здійснюється методом call();
  • JavaFileManager управляє читанням і записом у файли при компіляції;
  • JavaFileObject – файловий об'єкт, який дозволяє абстрагуватися від вихідного коду Java і файлів класів;
  • DiagnosticListener – слухач діагностичних подій компіляції.

Якщо сирцевий код береться не з файлової системи, то необхідно створити клас, який реалізує інтерфейс JavaFileObject. Java надає просту реалізацію у вигляді класу SimpleJavaFileObject. Можна створити похідний клас і перекрити методи відповідно до необхідності.

2.2 Високорівневі засоби роботи з потоками виконання

2.2.1 Недоліки низькорівневих засобів підтримки багатопотоковості. Огляд високорівневих засобів

Використання низькорівневих засобів забезпечує повний доступ до можливостей багатопотоковості, що надаються сучасними операційними системами. Проте низькорівневому підходу властивий ряд недоліків:

  • тенденція до перемішування низькорівневого коду організації багатопотоковості з кодом, що відповідає за основну функціональність програми
  • неможливість перезапуску потоку, що зумовлює необхідність створення зайвих об'єктів
  • перевитрата динамічної пам'яті у зв'язку з особливостями розміщення об'єктів-потоків
  • складності в управлінні ресурсами, що розділяються
  • проблеми з недостатньою ефективністю synchronized-коду
  • відсутність ефективних методів боротьби із взаємними блокуваннями.

Особливо перелічені недоліки є відчутними при створенні великої кількості потоків виконання.

Починаючи з версії Java 5 програміст має можливість працювати з високорівневими засобами маніпулювання потоками виконання. Більшість із цих можливостей представлено в новому пакеті java.util.concurrent та вкладених у нього пакетах. До основних засобів пакета можна віднести такі:

  • робота з одиницями виміру часу
  • робота з атомарними операціями
  • управління створенням потоків
  • управління блокуваннями
  • управління ресурсами, що розділяються

Крім того, версії Java 7 додані засоби розгалуження та об'єднання потоків (fork/join framework). Ці засоби були розроблені спеціально для розпаралелювання рекурсивних завдань. Абстрактний клас ForkJoinTask є "легковагомим" аналогом потоку виконання. У поєднанні з класом ForkJoinPool цей клас дозволяє визначити велику кількість паралельних завдань, що істотно перевищує кількість реальних потоків виконання (threads).

Існують також високорівневі механізми роботи з потоками виконання у JavaFX, представлені в пакеті javafx.concurrent: абстрактний клас Task, похідний від java.util.concurrent.FutureTask, своєю чергою, передбачає створення похідного класу, який реалізує метод call(), в якому реалізована взаємодія з візуальними компонентами. Крім того, цей клас надає можливість безпосереднього зв'язування деяких компонентів (наприклад, ProgressBar) з задачею (Task).

2.2.2 Робота з переліком TimeUnit

Перелік java.util.concurrent.TimeUnit використовують для представлення одиниць виміру проміжків часу. Він реалізує утиліти для перетворення часу з одних одиниць на інші. У наведеному нижче прикладі демонструються деякі можливості перетворення одиниць часу:

package ua.inf.iwanoff.java.advanced.additional;

import java.util.concurrent.TimeUnit;

public class TimeTest {
    static void main() {
        // Отримуємо час роботи Java VM (в наносекундах):
        long nanos = System.nanoTime();
        System.out.println(nanos);
        long micros = TimeUnit.NANOSECONDS.toMicros(nanos);
        System.out.println(micros);
        long millis = TimeUnit.MICROSECONDS.toMillis(micros);
        System.out.println(millis);
        long seconds = TimeUnit.MILLISECONDS.toSeconds(millis);
        System.out.println(seconds);
        long minutes = TimeUnit.SECONDS.toMinutes(seconds);
        System.out.println(minutes);
        long hours = TimeUnit.NANOSECONDS.toMillis(minutes);
        System.out.println(hours);
        long days = TimeUnit.HOURS.toDays(hours);
        System.out.println(days);
    }
}

Існує також універсальний метод convert() із двома параметрами – тривалістю та одиницею вимірювання часу, що дозволяє виконати аналогічні дії.

Цей перелік використовується переважно в контексті багатопотоковості. У переліку, зокрема, визначено методи timedWait(), timedJoin() та sleep(). Наприклад,

TimeUnit.SECONDS.sleep(2);

забезпечує "засипання" на 2 секунди. Як і для Thread.sleep(), потрібно перехоплення винятку InterruptedException.

2.2.3 Робота з атомарними операціями

Для того, щоб уникнути блокування, пакет java.util.concurrent.atomic надає так звані атомарні операції – діі, які виконуються як єдине, неподільне ціле. Для інших потоків у програмі атомарна операція або виглядає як повністю завершена, або як така, що ще не починалася. Атомарні операції представлені класами AtomicBoolean, AtomicInteger, AtomicLong і AtomicReference. Для класів, що представляють атомарні операції над цілими, реалізовано ряд методів, найбільш уживані з яких – set(), getAndSet(), compareAndSet(), incrementAndGet(), decrementAndGet(), incrementAndGet(), decrementAndGet(). Відповідні дії над змінними виконуються із забезпеченням безпеки, але без застосування блокувань. У наведеному нижче прикладі здійснюється робота з AtomicLong:

package ua.inf.iwanoff.java.advanced.additional;

import java.util.concurrent.atomic.AtomicLong;

public class AtomicLongDemo {
    static void main() {
        AtomicLong result = new AtomicLong(1);
        for (AtomicLong index = new AtomicLong(1); index.get() <= 20;
                                          index.incrementAndGet()) {
            result.set(result.get() * index.get());
        }
        System.out.println(result);
    }
}

Як видно з прикладу, здійснюється обчислення факторіала 20.

2.2.4 Управління створенням потоків. Пули потоків

Під час створення великих застосунків доцільно відокремити створення та керування потоками від коду іншої програми. Для цього створюють пули потоків – колекції заздалегідь створених потоків, готових виконувати завдання. Для створення пулів потоків у пакеті concurrent оголошено інтерфейси Executor, ExecutorService і ScheduledExecutorService, які декларують можливості реалізації створення пулів черг. Узагальнений інтерфейс Callable представляє деяку функцію та аналогічний Runnable, але дозволяє повертати деякий результат та генерувати винятки. Узагальнений інтерфейс Future використовують для представлення результату асинхронної операції submit(), визначеної в ExecutorService.

Для того, щоб скористатися можливостями пулу потоків, слід створити об'єкт класу ExecutorService. Найкращий варіант створення – скористатися одним із фабричних методів класу Executors.

У наведеному нижче прикладі створюються три потоки виконання, в яких виводяться відповідні повідомлення:

// Параметр - кількість потоків у пулі:
ExecutorService service = Executors.newFixedThreadPool(3); 
service.execute(() -> System.out.println("First"));
service.execute(() -> System.out.println("Second"));
service.execute(() -> System.out.println("Third"));

Для коректної зупинки потоків слід скористатися функцією shutdown():

service.shutdown();

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

2.2.5 Високорівневі засоби керування блокуваннями

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

Інтерфейс java.util.concurrent.locks.Lock підтримує високорівневі засоби керування блокуванням. Клас java.util.concurrent.locks.ReentrantLock, що реалізує даний інтерфейс, підтримує обмежені очікування зняття блокування, спроби блокування, що перериваються, черги блокування й установку очікування зняття декількох блокувань. Використання об'єктів блокування замість низькорівневих засобів, зокрема, запобігає взаємним блокуванням (deadlocks).

Зокрема, так можна встановити блокування (запобігти виконанню фрагмента коду для інших ниток) за допомогою об'єкта класу java.util.concurrent.locks.ReentrantLock (альтернатива synchronized). У наведеному нижче прикладі гарантується проведення операції зі зняття коштів з рахунку лише одним із клієнтів.

private Lock accountLock = new ReentrantLock();
  
public void withdrawCash(int accountID, int amount) {
    // Тут може бути потоковобезпечний код,
    // наприклад, читання з файлу чи бази даних

    accountLock.lock(); // заблокувати, коли сюди потрапляє потік  
    try {
        if (allowTransaction) { // якщо дозволено транзакцію
            updateBalance(accountID, amount, "Вилучення"); // зміна балансу
        }
        else {
            System.out.println("Недостатньо коштів");
        }
    }
    finally {
        accountLock.unlock(); // дозволити іншим потокам змінювати баланс
    }
}

Метод tryLock() намагається негайно заблокувати екземпляр блокування. Цей метод повертає true, якщо блокування пройшло успішно, і false, якщо екземпляр вже заблокований. Можна використовувати час затримки блокування та одиницю виміру:

Lock lock = ...;
if (lock.tryLock(50L, TimeUnit.MILLISECONDS)) ...

2.2.6 Використання синхронізаторів

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

  • Semaphore (Семафор) використовується для обмеження кількості потоків під час роботи з певними ресурсами. Доступ до загального ресурсу управляється за допомогою лічильника. Якщо лічильник більше нуля, доступ потоку дозволяється, а значення лічильника зменшується. Якщо лічильник дорівнює нулю, поточний потік блокується, поки інший потік не звільнить ресурс.
  • CountDownLatch (замок зі зворотним відліком) дозволяє одному або декільком потокам чекати доти, доки не завершиться певна кількість операцій, що виконують в інших потоках. У конструкторі класу вказується кількість допустимих операцій. Відповідні операції повинні забезпечувати зменшення лічильника викликом методу countDown(). Коли лічильник досягає 0, всі потоки, що очікують, розблокуються і продовжують виконуватися.
  • CyclicBarrier (Циклічний бар'єр) являє собою точку синхронізації, в якій задана кількість паралельних потоків зустрічається і блокується. Як тільки всі потоки прибули, виконується деяка дія (або не виконується, якщо бар'єр був ініціалізований без неї), і після того, як її виконано, потоки, що очікують, звільняються.
  • Phaser аналогічний циклічному бар'єру, але є гнучкішим і надає більшу функціональність.
  • Exchanger (Обмінник) дозволяє обмінюватися даними між двома потоками в певній точці роботи. Обмінник є узагальненим класом, що параметризується типом об'єкта для передачі. Потік, що викликає метод exchange() обмінника, блокується і чекає на інший потік. Коли інший потік викликає той самий метод, відбудеться обмін об'єктами.

2.3 Використання колекцій, безпечних з точки зору багатопотоковості

2.3.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 (Список з пропусками). Елементи асоціативного масиву впорядковані за ключами. У прикладі 3.2 продемонстровано роботу з ConcurrentSkipListMap.

2.3.2 Робота зі спеціальними колекціями, які використовують блокування

Пакет java.util.concurrent надає набір колекцій, безпечних з точки зору багатопотоковості. Це такі узагальнені типи, як BlockingQueue (інтерфейс) і його стандартні реалізації (ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue, DelayQueue), похідні від нього інтерфейси TransferQueue (реалізація LinkedTransferQueue) і LinkedBlockingDeque (стандартна реалізація – LinkedBlockingDeque). Колекції реалізовані в пакеті java.util.concurrent.

Інтерфейс BlockingQueue представляє чергу, яка дозволяє додавати й вилучати елементи безпечно з точки зору потоків. Типове використання BlockingQueue – занесення об'єктів одним потоком, а вилучення – іншим.

Потік, який додає об'єкти (перший потік), продовжує додавати їх в чергу, доки не буде досягнута деяка верхня межа припустимої кількості елементів. У цьому випадку під час спроби додати нові об'єкти перший потік блокується, поки потік виконання, який отримує елементи (другий потік) не вилучить з черги принаймні один елемент. Другий потік може вилучати й використовувати об'єкти з черги. Якщо, проте, другий потік спробує отримати об'єкт з порожньої черги, цей потік блокується, поки перший потік не додасть новий об'єкт.

Інтерфейс BlockingQueue походить від java.util.Queue, тому він підтримує методи add(), remove(), element() (з генерацією винятку), а також offer(), poll() и peek() (з поверненням спеціального значення). Крім того, визначені методи, які блокують – put() (для додавання) і take() (для вилучення). Є також методи, які використовують блокування з часовим інтервалом – offer()і poll() з додатковими параметрами (інтервалом часу й одиницею часу).

До BlockingQueue не можна додати значення null. Така спроба призводить до генерації винятку NullPointerException.

Найбільш популярна реалізація BlockingQueue – це ArrayBlockingQueue. Для зберігання посилань на об'єкти в цій реалізації використовують звичайний масив обмеженої довжини. Ця довжина після створення об'єкта не може бути змінена. Всі конструктори класу першим параметром отримують цю довжину масиву. У наведеному нижче прикладі створюються два потоки – виробник (Producer) і споживач (Consumer). Виробник додає цілі числа з затримкою між додаваннями, а споживач їх вилучає.

package ua.inf.iwanoff.java.advanced.additional;

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 {

    static void main() 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.4 Стандартні засоби створення графіків і діаграм

Стандартні засоби JavaFX надають готові компоненти для побудови графіків і діаграм. Абстратний клас Chart є базовим для всіх графіків і діаграм і об'єднує в собі властивості всіх компонентів, призначених для візуалізації числових даних. Безпосередньо похідними від нього є класи PieChart (кругова діаграма) та XYChart. Останній також є абстрактним і виступає базовим для всіх інших діаграм: AreaChart, BarChart, BubbleChart, LineChart, ScatterChart, StackedAreaChart і StackedBarChart.

Конструктори класів, похідних від XYChart, вимагають визначення двох об'єктів типу Axis (вісь). Axis – це абстрактний клас для представлення осі на графіку або діаграмі. Похідними від нього є клас CategoryAxis, об'єкти якого використовують, коли вздовж осі розташовують мітки у вигляді набору рядків, а також абстрактний клас ValueAxis. Клас NumberAxis, похідний від ValueAxis, використовують, коли вісь представляє шкалу числових значень.

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

package ua.inf.iwanoff.java.advanced.additional;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class ChartDemo extends Application {

    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Chart Demo");

        PieChart pieChart = new PieChart();
        pieChart.setMinWidth(300);
        pieChart.setTitle("Pie Chart");
        pieChart.getData().addAll(new PieChart.Data("two", 2),
                                  new PieChart.Data("three", 3),
                                  new PieChart.Data("one", 1));

        XYChart.Data<String, Number>[] barData = new XYChart.Data[] {
                                  new XYChart.Data<>("one", 2.0),
                                  new XYChart.Data<>("two", 3.0),
                                  new XYChart.Data<>("three", 1.0) };
        BarChart<String, Number> barChart = new BarChart<>(new CategoryAxis(), new NumberAxis());
        barChart.setTitle("Bar Chart");
        barChart.getData().addAll(new Series<>("Bars", FXCollections.observableArrayList(barData)));

        XYChart.Data<Number, Number>[] xyData = new XYChart.Data[] {
                new XYChart.Data<>(1.0, 2.0),
                new XYChart.Data<>(2.0, 3.0),
                new XYChart.Data<>(3.0, 1.0) };
        AreaChart<Number, Number> areaChart = new AreaChart<>(new NumberAxis(), new NumberAxis());
        areaChart.setTitle("Area Chart");
        areaChart.getData().addAll(new Series<>("Points", FXCollections.observableArrayList(xyData)));
        // Використовує той самий набір даних
        BubbleChart<Number, Number> bubbleChart = new BubbleChart<>(
                new NumberAxis(0, 4, 1), new NumberAxis(0, 5, 1));
        bubbleChart.setTitle("Bubble Chart");
        bubbleChart.getData().addAll(new Series<>("Bubbles", FXCollections.observableArrayList(xyData)));

        HBox hBox = new HBox();
        hBox.getChildren().addAll(pieChart, barChart, areaChart, bubbleChart);
        hBox.setSpacing(10);
        hBox.setPadding(new Insets(10, 10, 10, 10));

        Scene scene = new Scene(hBox,  1000, 350);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    static void main(String[] args) {
        launch(args);
    }
}

Головне вікно застосунку виглядатиме так:

До діаграм, похідних від XYChart, можна визначити кілька наборів даних (Series) для візуального порівняння різних залежностей.

Особливу цікавість представляє діаграма LineChart, яка може бути використана для побудови графіків функцій. Для побудови графіку спочатку слід підготувати дані залежності у вигляді набору точок. Точок повинно бути достатньо багато, щоб крива була гладкою, але не надто багато, щоб не уповільнити виконання програми. У наведеній вище програмі здійснюється побудова двох тригонометричних функцій на одному графіку:

package ua.inf.iwanoff.java.advanced.additional;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;

import java.util.function.DoubleUnaryOperator;

public class LineChartDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Тригонометричні функції");
        Scene scene = new Scene(fgGraph(-4, 4, 1, -1.5, 1.5, 1,
                "Синус", Math::sin, "Косинус", Math::cos), 800, 350);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private LineChart<Number, Number> fgGraph(double xFrom, double xTo, double xStep,
                                              double yFrom, double yTo, double yStep,
                                              String fName, DoubleUnaryOperator f,
                                              String gName, DoubleUnaryOperator g) {
        NumberAxis xAxis = new NumberAxis(xFrom, xTo, xStep);
        NumberAxis yAxis = new NumberAxis(yFrom, yTo, yStep);
        LineChart<Number, Number> graphChart = new LineChart<>(xAxis, yAxis);
        // Вимикаємо "символи" в точках
        graphChart.setCreateSymbols(false);
        double h = (xTo - xFrom) / 100;
        // Додаємо ім'я і точки першої функції:
        XYChart.Series<Number, Number> fSeries = new XYChart.Series<>();
        fSeries.setName(fName);
        for (double x = xFrom; x <= xTo; x += h) {
            fSeries.getData().add(new XYChart.Data<>(x, f.applyAsDouble(x)));
        }
        // Додаємо ім'я і точки другої функції:
        XYChart.Series<Number, Number> gSeries = new XYChart.Series<>();
        gSeries.setName(gName);
        for (double x = xFrom; x <= xTo; x += h) {
            gSeries.getData().add(new XYChart.Data<>(x, g.applyAsDouble(x)));
        }
        // Додаємо обидві функції
        graphChart.getData().addAll(fSeries, gSeries);
        return graphChart;
    }

    static void main(String[] args) {
        launch(args);
    }
}

Головне вікно виглядатиме так:

 

3 Приклади програм

3.1 Обчислення значення виразу з рядком

Під час виконання програми користувач вводить вираз, в якому використовується деякий рядок, і отримує результат цього виразу на екрані. Можна використовувати методи класу String. Залежно від мети використання програми можна запропонувати два варіанти її реалізації.

Перший варіант

Перший варіант передбачає, що генерація коду з уведеним користувачем рядком під час виконання програми здійснюватиметься один раз. Припустимо, ми створили проєкт StringProcessor, пакет ua.inf.iwanoff.java.advanced.additional і клас StringProcessor. Код може бути таким:

package ua.inf.iwanoff.java.advanced.additional;

import java.io.*;
import java.lang.reflect.Method;
import java.util.Scanner;
import javax.tools.*;

public class StringProcessor {
    final String sourceFile = "target/classes/ua/inf/iwanoff/java/advanced/additional/StrFun.java";

    void genSource(String expression) {
        try (PrintWriter out = new PrintWriter(sourceFile)) {
            out.println("package ua.inf.iwanoff.java.advanced.additional;");
            out.println("public class StrFun {");
            out.println("    public static String transform(String s) {");
            // Додаємо порожній рядок, щоб перетворити на рядок результат будь-якого типу:
            out.println("        return " + expression + " + \"\";");
            out.println("    }");
            out.println("}");
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

    boolean compile() {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        return compiler.run(null, null, null, sourceFile) == 0;
    }

    static void main() {
        StringProcessor sp = new StringProcessor();
        Scanner scan = new Scanner(System.in);
        System.out.println("Уведіть вираз, який треба виконати над рядком s:");
        String expression = scan.nextLine().replaceAll("\"", "\\\"");
        sp.genSource(expression);
        try {
            if (sp.compile()) {
                System.out.println("Введіть рядок s:");
                String s = scan.nextLine();
                Class<?> cls = Class.forName("ua.inf.iwanoff.java.advanced.additional.StrFun");
                Method m = cls.getMethod("transform", String.class);
                System.out.println(m.invoke(null, new Object[] { s }));
            }
            else {
                System.out.println("Помилка введення виразу!");
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Другий варіант

Складніший випадок передбачає багаторазове уведення виразу і, відповідно, багаторазову генерацію коду. Проблема полягає в тому, що після завантаження класу його треба видалити (unload), для того, щоб завантажити нову версію з таким же ім'ям. Це нетривіальна задача, яка передбачає створення власного завантажувача. Простіший підхід полягає у створенні класів з різними іменами. Наприклад, до імен класів можна додавати послідовні цілі числа. Тепер програма буде такою:

package ua.inf.iwanoff.java.advanced.additional;

import java.io.*;
import java.lang.reflect.Method;
import java.util.Scanner;
import javax.tools.*;

public class StringProcessor {
    final String sourceFile = "target/classes/ua/inf/iwanoff/java/advanced/additional/StrFun";

    void genSource(String expression, int number) {
        try (PrintWriter out = new PrintWriter(sourceFile + number + ".java")) {
            out.println("package ua.inf.iwanoff.java.advanced.additional;");
            out.println("public class StrFun {");
            out.println("   public static String transform(String s) {");
            out.println("        return " + expression + " + \"\";");
            out.println("    }");
            out.println("}");
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }

    boolean compile(int number) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        return compiler.run(null, null, null, sourceFile + number + ".java") == 0;
    }

    static void main() {
        int number = 0;
        StringProcessor sp = new StringProcessor();
        Scanner scan = new Scanner(System.in);
        String expression;
        do {
            System.out.println("Уведіть вираз, який треба виконати над рядком s "
                    + "(порожній рядок для закінчення):");
            expression = scan.nextLine().replaceAll("\"", "\\\"");
            if (expression.isEmpty()) {
                break;
            }
            sp.genSource(expression, ++number);
            try {
                if (sp.compile(number)) {
                    System.out.println("Введіть рядок s:");
                    String s = scan.nextLine();
                    Class<?> cls = Class.forName("ua.inf.iwanoff.java.advanced.additional.StrFun" + number);
                    Method m = cls.getMethod("transform", String.class);
                    System.out.println(m.invoke(null, new Object[]{s}));
                }
                else {
                    System.out.println("Помилка введення виразу!");
                }
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
        while (!expression.isEmpty());
    }

}

3.2 Робота з контейнером ConcurrentSkipListMap

У наведеній нижче програмі один потік виконання здійснює заповнення ConcurrentSkipListMap парами число / список простих множників, а інший потік знаходить в цьому списку прості числа (числа з одним простим співмножником) і заносить їх до списку. Числа перевіряються в заданому діапазоні.

Клас PrimeFactorization (розкладання на прості множники):

package ua.inf.iwanoff.java.advanced.additional;

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.inf.iwanoff.java.advanced.additional;

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.inf.iwanoff.java.advanced.additional;

public class PrimesTest {
    static void main() 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);
    }
}

4 Вправи для контролю

  1. Створити консольний застосунок, в якому здійснюється демонстрація можливостей ConcurrentHashMap і ConcurrentSkipListMap.
  2. Створити програму графічного інтерфейсу користувача, яка призначена для побудови графіку довільних функцій. Для реалізації використовувати засоби інтерпретації скриптів JavaScript.

5 Контрольні запитання

  1. Що таке метапрограмування?
  2. Як в код на Java включити код на JavaScript?
  3. Як програмно здійснити компіляцію сирцевого коду?
  4. У чому є недоліки низькорівневих засобів підтримки багатопотоковості?
  5. Що таке атомарна операція?
  6. Що таке пул потоків?
  7. Які контейнерні класи забезпечують безпеку з точки зору потоків?
  8. Як отримати синхронізовані аналоги стандартних колекцій?
  9. Для чого використовують контейнери ConcurrentHashMap і ConcurrentSkipListMap?
  10. Для чого використовують контейнер BlockingQueue?
  11. Як у JavaFX використовують стандартні засоби побудови графіків і діаграм?

 

up