Потоки. Клас Thread.Створення потоків
Насамперед зазначимо, що два англомовних терміна “thread” та “stream” перекладаються українською як потік. Про потоки “thread” ми вже говорили, зараз будемо знайомитися з потоками “stream”. Щоб уникнути плутанини, надалі, говорячи про потоки “thread”, будемо додавати прикметник “обчислювальний”.
Отже, які типи потоків пропонує нам Java.
Потоки, пов’язані з локальними файлами;
Потоки, пов’язані з даними в ОП;
Канальні потоки, тобто потоки для передачі даних між різними обчислювальними потоками;
Стандартні потоки введення-виведення.
Природно, що основну увагу нами буде приділено файловим потокам.
Як і можна було очікувати, всі потокові класи походять безпосередньо від класу Object (рис. 9):
Рисунок 9 – Основні потокові класи
Створення потоків
Для реалізації багатопоточності ми маємо скористатися класом java.lang.Thread. В ньому визначено всі методи для створення потоків, управління їх станом та синхронізації.
Є дві можливості для того, щоб дати можливість вашим класам працювати в різних потоках.
По-перше, можна створити свій клас-нащадок від суперкласа Thread. При цьому ви отримуєте безпосередній доступ до всіх методів потоків:
public class MyClass extends Thread.
По-друге, ваш клас може реалізувати інтерфейс Runnable. Це – ліпший варіант, якщо ви бажаєте розширити властивості якогось іншого класу, наприклад, Frame, як ми це робили раніше, або Applet. Оскільки в Java немає множинної спадковості, реалізація інтерфейса Runnable – єдина можливість рішення цієї проблеми.
public class MyClass extends Frame implements Runnable.
І в тому, і в іншому випадку вам доведеться реалізовувати метод run().
Є сім конструкторів в класі Thread. Найчастіше застосовується один з них, а саме з одним параметром-посиланням на об'єкт, для якого буде викликатися метод run(). При використанні інтерфейсу Runnable метод run() визначено у головному класі додатка, тому як параметр конструктору передається значення посилання на цей клас (this).
Як бачите, ми знову згадали про метод run(). Мабуть, зараз дехто думає: саме середовище Windows періодично викликає метод run() – і помиляється. Насправді метод run() отримує управління при запуску потоку методом start() (безпосередньо не викликається!).
- Керування потоками. Клас Runnable. Основні методи.
Керування потоками
Після того як потік створений, над ним можна виконувати різні керуючі операції: запуск, зупинку, тимчасову зупинку і т.д. Для цього необхідно використовувати методи, визначені в класі Thread.
Запуск потоку. Для запуску потоку на виконання ви повинні викликати метод start():
public void start();
Як тільки додаток викликає цей метод для об'єкта класу Thread чи для об'єкта класу, що реалізує інтерфейс Runnable, керування отримує метод run(), визначений у відповідному класі.
Якщо метод run() повертає керування, запущений потік завершує свою роботу. Однак, звичайно метод run() запускає нескінченний цикл, тому потік не завершить своє виконання доти, поки він не буде зупинений (чи завершений) примусово. Щоб визначити, запущений даний чи потік ні, можна скористатися таким методом:
public final boolean isAlive();
Зупинка потоку. Якщо додаток бажає зупинити потік нормальним неаварійним чином, то він викликає для відповідного об'єкта метод stop():
public final void stop();
Тимчасова зупинка і поновлення роботи потоку. За допомогою метода sleep() ви можете затримати виконання потоку на заданий час (в мілісекундах):
public static void sleep(long ms);
При цьому управління передається іншим потокам. Роботу ж нашого потоку буде поновлено через заданий інтервал часу. Метод suspend() тимчасово припиняє роботу потоку:
public final void suspend();
Для продовження роботи потоку необхідно викликати метод resume():
public final void resume();
Пріоритети потоків. Клас Thread містить методи що дозволяють управляти пріоритетами потоків. Є також засоби для синхронізації та блокування потоків.
1) Таким чином, якщо необхідно запустити один потік для анімації, слід виконати такі дії:
2) В об’яві класу вказати, що він реалізує інтерфейс Runnable.
3) Описати змінну класу Thread як поле класу.
4) Описати метод start() в середині вашого класу. Ця вимога не є обов’язковою, але якщо ви потім захочете перетворити ваш додаток на аплет, ліпше зробити саме так. В методі start() створити об’єкт класу Thread (за допомогою new) і викликати метод start() для цього об’єкта:
myThread.start()
Сам метод start()
MyClass.start()
для додатка можна викликати з методу main(). Для аплета він викликається автоматично.
5) Описати метод run() в середині вашого класу. В середині метода run() створити “нескінчений” цикл. В циклі має знаходитися перерисовка зображення (метод repaint()) і призупинка роботи потоку на деякий час (метод sleep()), щоб інші потоки також могли виконати свої функції.
6) Завершити роботу потоку можна за допомогою метода stop().
Якщо наш додаток повинен запускати декілька потоків, варто скористатися іншою технікою. Вона полягає в тому, що ми створюємо один чи декілька класів на базі класу Thread або з використанням інтерфейсу Runnable. Кожен такий клас відповідає одному потоку і має свій власний метод run(). У класі аплета нам потрібно визначити необхідну кількість об'єктів класу, що реалізує потік, при цьому інтерфейс Runnable в самому аплеті реалізовувати не потрібно.
- Виняткові ситуації. Класи Throwable, Exception, Error. 29. Методи обробки виняткових ситуацій.
Виняток або виключення (exception) –це спеціальний тип помилки, який виникає у випадку неправильної роботи програми. Виняткова (виключна) ситуація може виникнути при роботі Java-програми в результаті, наприклад, ділення на нуль або може бути ініційована програмно в середині метода деякого класу. Прикладом такого винятку, що генерується програмно, може служити FileNotFoundException, який викидається (throw) методами класів введення-виведення при спробі відкрити неіснуючий файл. Замість терміну “викидається” часто вживають синоніми: збуджується, генерується, ініціюється.
Після того як Java-машина створить об’єкт-виняток, цей об’єкт пересилається додатку. Об’єкт, що утворюється при збудженні винятку, несе інформацію про виняткову ситуацію (точка виникнення, опис тощо). Використовуючи методи цього об’єкта можна, наприклад, вивести на екран або в файл інформацію про цей виняток.
Додаток має перехопити виняток. Для перехоплення використовується так званий try-catch блок. Наприклад, при спробі читання даних з потоку стандартного пристрою введення-виведення може виникнути виняток IOException:
try {
System.in.read(buffer, 0, 255);
. . .
}
catch (IOException e) {
String err = e.toString();
System.out.println(err);
}
Якщо читання не вдалося, Java ігнорує всі інші оператори в блоці try і переходить на блок catch, в якому програма обробляє виняток. Якщо все відбувається нормально, весь код в середині блока try виконується, а блок catch пропускається.
Зверніть увагу. Блок catch нагадує метод, адже йому передається як параметр об’єкт-виняток. Тип параметра – IOException, ім’я параметра – e. В об’єктному світі Java живуть майже самі об’єкти. І e – це також об’єкт класу IOException. Можна звернутися до методів цього об’єкта, наприклад, щоб отримати інформацію про виняток. Це і відбувається в нашому прикладі (метод toString()).
Що буде, якщо не перехопити виняток? В принципі, нічого особливого. Просто виконання даного потоку команд припиниться та буде виведено системне повідомлення на консоль. Роботу програми при цьому, можливо, буде завершено, а, можливо, і ні (якщо програма має декілька потоків виконання – наприклад, діалогові програми, не завершуються, а лише видають повідомлення на консоль. Погано, що ці повідомлення можна навіть не побачити).
Отже, якщо в тому методі, де виникла виняткова ситуація, немає блока його перехоплення, то метод припиняє свою роботу. Якщо в методі, з якого викликано даний метод, також немає блока перехоплення, то і він припиняє свою роботу. І т. д., поки не буде знайдено блок перехоплення або не закінчиться ланцюжок викликаних методів.
Звідси висновок: обробляти виняток необов’язково в тому ж самому методі, в якому він генерується.
Розглянемо приклад того, як виникають виняткові ситуації та як їх можна проаналізувати та обробити. Пригадаємо нашу першу діалогову програму, точніше метод actionPerformed() з неї:
public void actionPerformed(ActionEvent evt)
{
String arg = evt.getActionCommand();
if ("Check".equals(arg))
{
String str1 = textField1.getText();
String str2 = textField2.getText();
int first = Integer.parseInt(str1);
int second = Integer.parseInt(str2);
String answer = "No";
if (first*first==second || second*second==first)
{ answer = "Yes" ; }
displayStr = "Check Square - " + answer;
repaint();
}
}
Навіть в такій простій програмі можуть виникнути винятки, пов’язані з введенням даних користувачем. Отже, користувач натискає кнопку Check, а дані не введено. Маємо виняткову ситуацію NumberFormatException, про яку сповіщає Java. Давайте змінимо програму, щоб вона перехоплювала виняток:
public void actionPerformed(ActionEvent evt)
{
String arg = evt.getActionCommand();
if ("Check".equals(arg))
{
String str1 = textField1.getText();
String str2 = textField2.getText();
try {
int first = Integer.parseInt(str1);
int second = Integer.parseInt(str2);
String answer = "No";
if (first*first==second || second*second==first)
answer = "Yes" ;
displayStr = "Check Square - " + answer;
}
catch (NumberFormatException e) {
displayStr = “Input format error;
}
repaint();
}
}
Тепер все гаразд, заодно ми запобігли і неправильному введенню (букви замість цифр).
Якщо додати ще один рядок
first=first/second;
(просто так, логіка програми цього не вимагає), ми отримаємо ще один виняток, на цей раз ArithmeticException. Його також можна обробити:
public void actionPerformed(ActionEvent evt)
{
String arg = evt.getActionCommand();
if ("Check".equals(arg))
{
String str1 = textField1.getText();
String str2 = textField2.getText();
try {
int first = Integer.parseInt(str1);
int second = Integer.parseInt(str2);
String answer = "No";
first = first/second;
if (first*first==second || second*second==first)
answer = "Yes" ;
displayStr = "Check Square - " + answer;
}
catch (NumberFormatException e) {
displayStr = “Input format error”;
}
catch (ArithmeticException e) {
displayStr = “Division by zero”
}
repaint();
}
}
Два блоки catch ідуть один за одним, щоб обробити кожен виняток з блоку try.
При написанні власних методів ви також маєте враховувати, що вони можуть збуджувати винятки. В цьому випадку метод має виглядати так:
public int fact(int num) throws IllegalArgumentException
{
if ( num < 0 || num>10)
{
throw new IllegalArgumentException(“Number out of range”);
}
int res=1;
for (int i =1; i<=num; i++)
res*= i;
return res;
}
Оскільки в методі fact() виняток не оброблявся, його має обробити метод, який викликав fact():
try {
f = fact(x);
. . .
}
catch (IllegalArgumentException e) {
displayStr=e.toString();
}
Можна створювати власні класи винятків, але це зовсім інша тема [2].
Підводячи підсумки, можна сказати, що існує два варіанти генерації винятків: автоматична генерація (наприклад, IOException, ArithmeticException) та явна програмна генерація за допомогою оператора throw: наприклад, throw new IllegalArgumentException. В будь-якому випадку програма має перехопити виняток та обробити його в блоці try-catch.