Нить thread, runnable

Багатопотокове програмування дозволяє розділити уявлення і обробку інформації на кілька «легких» процесів (light-weight processes), що мають загальний доступ як до методів різних об'єктів додатки, так і до їхніх полів. Нить незамінна в тих випадках, коли графічний інтерфейс повинен реагувати на дії користувача при виконанні певної обробки інформації. Потоки можуть взаємодіяти один з одним через основний «батьківський» потік, з якого вони стартували.

Як приклад можна привести деякий потік, який відповідає за надання інформації в інтерфейсі, який очікує завершення роботи іншого потоку, що завантажує файл, і одночасно відображає деяку анімацію або оновлює прогрес-бар. Крім того цей потік може зупинити завантажує файл потік при натисканні кнопки «Скасувати».

Творці Java надали дві можливості створення потоків: реалізація (implementing) інтерфейсу Runnable і розширення (extending) класу Thread. Розширення класу - це шлях успадкування методів і змінних класу батька. В цьому випадку можна успадковуватися тільки від одного батьківського класу Thread. Дане обмеження всередині Java можна подолати реалізацією інтерфейсу Runnable. який є найбільш поширеним способом створення потоків.

Переваги потоків перед процесами

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

головний потік

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

клас Thread

У класі Thread визначені сім перевантажених конструкторів, велика кількість методів, призначених для роботи з потоками, і три константи (пріоритети виконання потоку).

Конструктори класу Thread

  • target - екземпляр класу реалізує інтерфейс Runnable;
  • name - ім'я створюваного потоку;
  • group - група до якої належить потік.

Приклад створення потоку, який входить до групи, реалізує інтерфейс Runnable і має свою унікальну назву:

Групи потоків зручно використовувати, коли необхідно однаково управляти декількома потоками. Наприклад, кілька потоків виводять дані на друк і необхідно перервати друк всіх документів поставлених в чергу. У цьому випадку зручно застосувати команду до всіх потоків одночасно, а не до кожного потоку окремо. Але це можна зробити, якщо потоки віднесені до однієї групи.

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

Методи класу Thread

Найбільш часто використовувані методи класу Thread для управління потоками:

  • long getId () - отримання ідентифікатора потоку;
  • String getName () - отримання імені потоку;
  • int getPriority () - отримання пріоритету потоку;
  • State getState () - визначення стану потоку;
  • void interrupt () - переривання виконання потоку;
  • boolean isAlive () - перевірка, чи виконується потік;
  • boolean isDaemon () - перевірка, чи є потік «Daemon»;
  • void join () - очікування завершення потоку;
  • void join (millis) - очікування millis мілісекунд завершення потоку;
  • void notify () - «пробудження» окремого потоку, що очікує «сигналу»;
  • void notifyAll () - «пробудження» всіх потоків, які очікують «сигналу»;
  • void run () - запуск потоку, якщо потік був створений з використанням інтерфейсу Runnable;
  • void setDaemon (bool) - визначення потоку призначеним для користувача або «Daemon»;
  • void setPriority (int) - визначення пріоритету потоку;
  • void sleep (int) - припинення потоку на заданий час;
  • void start () - запуск потоку.
  • void wait () - припинення потоку, поки інший потік не викличе метод notify ();
  • void wait (millis) - припинення потоку на millis мілісекунд або поки інший потік не викличе метод notify ();

Життєвий цикл потоку

При виконанні програми об'єкт Thread може перебувати в одному з чотирьох основних станів: «новий», «працездатний», «непрацездатний» і «пасивний». При створенні потоку він отримує стан «новий» (NEW) і не виконується. Для перекладу потоку зі стану «новий» в «працездатний» (RUNNABLE) слід виконати метод start (), що викликає метод run ().

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

NEW - потік створений, але ще не запущений;
RUNNABLE - потік виконується;
BLOCKED - потік блокований;
WAITING - потік чекає заплати за працю іншого потоку;
TIMED_WAITING - потік деякий час чекає закінчення іншого потоку;
TERMINATED - потік завершено.

Приклад використання Thread

У прикладі ChickenEgg розглядається паралельна робота двох потоків (головний потік і потік Egg), в яких йде суперечка, «що було раніше, яйце чи курка?». Кожен потік висловлює свою думку після невеликої затримки, що формується методом ChickenEgg.getTimeSleep (). Перемагає той потік, який останнім говорить своє слово.

При виконанні програми в консоль було виведено наступне повідомлення.

Неможливо точно передбачити, який потік закінчить висловлюватися останнім. При наступному запуску «переможець» може змінитися. Це відбувається внаслідок так званого «асинхронного виконання коду». Асинхронність забезпечує незалежність виконання потоків. Або, іншими словами, паралельні потоки незалежні один від одного, за винятком випадків, коли бізнес-логіка залежності виконання потоків визначається передбаченими для цього коштів мови.

інтерфейс Runnable

Інтерфейс Runnable містить тільки один метод run ():

Метод run () виконується при запуску потоку. Після визначення об'єкта Runnable він передається в один з конструкторів класу Thread.

Приклад класу RunnableExample, що реалізує інтерфейс Runnable

При виконанні програми в консоль було виведено наступне повідомлення.

Синхронізація потоків, synchronized

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

У прикладі визначений загальний ресурс у вигляді класу CommonObject, в якому є цілочисельне поле counter. Даний ресурс використовується внутрішнім класом. що створює потік CounterThread для збільшення в циклі значення counter на одиницю. При старті потоку полю counter присвоюється значення 1. Після завершення роботи потоку значення res.counter має дорівнювати 4.

У головному класі програми SynchronizedThread.main запускається п'ять потоків. Тобто, кожен потік повинен в циклі збільшити значення res.counter з одиниці до чотирьох; і так п'ять разів. Але результат роботи програми, що відображається в консолі, буде іншим:

Тобто, із загальним ресурсів res.counter працюють всі потоки одночасно, по черзі змінюючи значення.

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

Блокування на рівні об'єкта

Наступний код демонструє порядок використання оператора synchronized для блокування доступу до об'єкта.

Блокування на рівні методу і класу

Блокувати доступ до ресурсів можна на рівні методу і класу. Наступний код показує, що якщо під час виконання програми є кілька примірників класу DemoClass, то тільки один потік може виконати метод demoMethod (), для інших потоків доступ до методу буде заблокований. Це необхідно коли потрібно зробити певні ресурси потокобезпечна.

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

Деякі важливі зауваження використання synchronized

  1. Синхронізація в Java гарантує, що два потоку не можуть виконати синхронізований метод одночасно.
  2. Оператор synchronized можна використовувати тільки з методами і блоками коду, які можуть бути як статичними, так і не статичними.
  3. Якщо один з потоків починає виконувати синхронізований метод або блок, то цей метод / блок блокуються. Коли потік виходить з синхронізованого методу або блоку JVM знімає блокування. Блокування знімається, навіть якщо потік залишає синхронізований метод після завершення через будь-яких помилок або виключень.
  4. Синхронізація в Java викликає виключення NullPointerException, якщо об'єкт, який використовується в синхронізований блоці, не визначений, тобто дорівнює null.
  5. Синхронізовані методи в Java вносять додаткові витрати на продуктивність програми. Тому слід використовувати синхронізацію, коли вона абсолютно необхідна.
  6. Відповідно до специфікації мови не можна використовувати synchronized в конструкторі, тому що приведе до помилки компіляції.

Взаємодія між потоками в Java, wait і notify

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

  • wait () - звільняє монітор і переводить викликає потік в стан очікування до тих пір, поки інший потік не викличе метод notify ();
  • notify () - продовжує роботу потоку, у якого раніше був викликаний метод wait ();
  • notifyAll () - відновлює роботу всіх потоків, у яких раніше був викликаний метод wait ().

Всі ці методи викликаються тільки з синхронізованого контексту (синхронізованого блоку або методу).

Розглянемо приклад «Виробник-Склад-Споживач» (Producer-Store-Consumer). Поки виробник не поставить на склад продукт, споживач не може його забрати. Припустимо виробник повинен поставити 5 одиниць певного товару. Відповідно споживач повинен весь товар отримати. Але, при цьому, одночасно на складі може знаходитися не більше 3 одиниць товару. При реалізації даного прикладу використовуємо методи wait () і notify ().

Лістинг класу Store

Клас Store містить два синхронізованих методу для отримання товару get () і для додавання товару put (). При отриманні товару виконується перевірка лічильника counter. Якщо на складі товару немає, то є counter <1, то вызывается метод wait(). который освобождает монитор объекта Store и блокирует выполнение метода get(). пока для этого монитора не будет вызван метод notify() .

При додаванні товару також виконується перевірка кількості товару на складі. Якщо на складі більше 3 одиниць товару, то поставка товару припиняється і викликається метод notify (). який передає управління методу get () для завершення циклу while ().

Лістинг класів Producer і Consumer

Класи Producer і Consumer реалізують інтерфейс Runnable. методи run () у них перевизначені. Конструктори цих класів як параметр отримують об'єкт склад Store. При старті даних об'єктів у вигляді окремих потоків в циклі викликаються методи put () і get () класу Store для «додавання» і «отримання» товару.

Лістинг класу Trade

У головному потоці класу Trade (в методі main) створюються об'єкти Producer-Store-Consumer і старт потоки виробника і споживача.

При виконанні програми в консоль будуть виведені наступні повідомлення:

Потік-демон, daemon

Java програма завершує роботу тоді, коли завершує роботу останній його потік. Навіть якщо метод main () вже завершився, але ще виконуються породжені їм потоки, система буде чекати їх завершення.

Однак це правило не стосується потоків-демонам (daemon). Якщо завершився останній звичайний потік процесу, і залишилися тільки daemon потоки, то вони будуть примусово завершені і виконання програми закінчиться. Найчастіше daemon потоки використовуються для виконання фонових завдань, які обслуговують процес протягом його життя.

Оголосити потік демоном досить просто. Для цього потрібно перед запуском потоку викликати його метод setDaemon (true). Перевірити, чи є потік daemon 'ом можна викликом методу isDaemon (). Як приклад використання daemon-потоку можна розглянути клас Trade, який прийняв би такий вигляд:

Тут можна самостійно поекспериментувати з визначенням daemon-потоку для одного з класів (producer, consumer) або обох класів, і подивитися, як система (JVM) буде вести себе.

Яку реалізацію вибрати?

Навіщо потрібно два види реалізації багатопоточності, і яку з них і коли використовувати? Відповідь проста. Реалізація інтерфейсу Runnable використовується у випадках, коли клас вже успадковує будь-якої батьківський клас і не дозволяє розширити клас Thread. До того ж хорошим тоном програмування в java вважається реалізація інтерфейсів. Це пов'язано з тим, що в java може успадковуватися тільки один батьківський клас. Таким чином, успадкувавши клас Thread. неможливо успадковувати будь-якої іншої клас.

Розширення класу Thread доцільно використовувати в разі необхідності перевизначення інших методів класу крім методу run ().

завантажити приклади

Розглянуті на сторінці приклади многопоточности і синхронізації потоків у вигляді проекту Eclipse можна скачати тут (14Кб).