Синхронізація потоків
Багато-режим роботи відкриває нові можливості для програмістів, однак за ці можливості доводиться розплачуватися ускладненням процесу проектування програми та налагодження. Основна складність, з якою стикаються програмісти, ніколи не створювали раніше багатопотокові програми, це синхронізація одночасно працюючих потоків.
Для чого і коли вона потрібна?
Однопоточні програма, така, наприклад, як програма MS-DOS, при запуску отримує в монопольне розпорядження всі ресурси комп'ютера. Так як в однопоточному системі існує тільки один процес, він використовує ці ресурси в тій послідовності, яка відповідає логіці роботи програми. Процеси і потоки, що працюють одночасно в багатопотокової системі, можуть намагатися звертатися одночасно до одних і тих же ресурсів, що може привести до неправильної роботи додатків.
Пояснимо це на простому прикладі.
Нехай ми створюємо програму, що виконує операції з банківським рахунком. Операція зняття деякої суми грошей з рахунку може відбуватися в такій послідовності:- на першому кроці перевіряється загальна сума грошей, яка зберігається на рахунку;
- якщо загальна сума дорівнює або перевищує розмір суми, що знімається грошей, загальна сума зменшується на необхідну величину;
- значення залишку записується на поточний рахунок.
Якщо операція зменшення поточного рахунку виконується в однопоточному системі, то ніяких проблем не виникне. Однак уявімо собі, що два процеси намагаються одночасно виконати тільки що описану операцію з одним і тим же рахунком. Нехай при цьому на рахунку знаходиться 5 млн. Доларів, а обидва процеси намагаються зняти з нього по 3 млн. Доларів.
Припустимо, події розгортаються так:- перший процес перевіряє стан поточного рахунку і переконується, що на ньому зберігається 5 млн. доларів;
- другий процес перевіряє стан поточного рахунку і також переконується, що на ньому зберігається 5 млн. доларів;
- перший процес зменшує рахунок на 3 млн. доларів і записує залишок (2 млн. доларів) на поточний рахунок;
- другий процес виконує ту ж саму операцію, так як після перевірки вважає, що на рахунку і далі зберігається 5 млн. доларів.
В результаті вийшло, що з рахунку, на якому перебувало 5 млн. Доларів, було знято 6 млн. Доларів, і при цьому там залишилося ще 2 млн. Доларів! Разом - банку завдано збитків в 3 млн. Доларів.
Як же скласти програму зменшення рахунку, щоб вона не дозволяла витворяти подібне?
Дуже просто - на час виконання операцій над рахунком одним процесом необхідно заборонити доступ до цього рахунку з боку інших процесів. В цьому випадку сценарій роботи програми повинен бути наступним:- процес блокує рахунок для виконання операцій іншими процесами, отримуючи його в монопольне володіння;
- процес проводить процедуру зменшення рахунку і записує на поточний рахунок нового значення залишку;
- процес розблокує рахунок, дозволяючи іншим процесам виконання операцій.
Коли перший процес блокує рахунок, він стає недоступний іншим процесам. Якщо другий процес також спробує заблокувати цей же рахунок, він буде переведений в стан очікування. Коли перший процес зменшить рахунок і на ньому залишиться 2 млн. Доларів, другий процес буде розблоковано. Він перевірить залишок, переконається, що сума недостатня і не буде проводити операцію.
Таким чином, в багатопотокової середовищі необхідна синхронізація потоків при зверненні до критичних ресурсів. Якщо над такими ресурсами будуть виконуватися операції в неправильній послідовності, це призведе до виникнення важко виявлених помилок.
У мові програмування Java передбачено кілька засобів для синхронізації потоків, які ми зараз розглянемо.
Синхронізація методів
Можливість синхронізації як би вбудована в кожен об'єкт, що створюється додатком Java. Для цього об'єкти забезпечуються засувками, які можуть бути використані для блокування потоків, які звертаються до цих об'єктів.
Щоб скористатися засувками, ви можете оголосити відповідний метод як synchronized. зробивши його синхронізованим:
При виклику синхронізованого методу відповідний йому об'єкт (в якому він визначений) блокується для використання іншими синхронізованими методами. В результаті запобігається одночасний запис двома методами значень в область пам'яті, що належить даному об'єкту.
Використання синхронізованих методів - досить простий спосіб синхронізації потоків, які звертаються до загальних критичним ресурсам, на зразок описаного вище банківського рахунку.
Зауважимо, що не обов'язково синхронізувати весь метод - можна синхронізувати тільки критичного фрагмента коду.
Тут синхронізація виконується для об'єкта Account.
Блокування потоку
Синхронізований потік, визначений як метод типу synchronized, може переходити в їхній заблокований статус автоматично при спробі звернення до ресурсу, зайнятого іншим синхронізованим методом, або при виконанні деяких операцій введення або виведення. Однак в ряді випадків корисно мати більш тонкі засоби синхронізації, що допускають явне використання програма потребуватиме цього.
Блокування на заданий період часу
За допомогою методу sleep можна заблокувати потік на заданий період часу:
В даному прикладі робота потоку Thread призупиняється на 500 мілісекунд. Зауважимо, що під час очікування призупинений потік не забирає ресурси процесора.
Так як метод sleep може створювати виключення InterruptedException, необхідно передбачити його обробку. Для цього ми використовували оператори try і catch.
Тимчасове призупинення і поновлення роботи
Методи suspend і resume дозволяють, відповідно, тимчасово припиняти і відновлювати роботу потоку.
У наступному фрагменті коду потік m_Rectangles призупиняє свою роботу, коли курсор миші виявляється над вікном аплета:
Робота потоку поновлюється, коли курсор миші покидає вікно аплета:
очікування сповіщення
Якщо вам потрібно організувати взаємодію потоків таким чином, щоб один потік керував роботою іншого або інших потоків, ви можете скористатися методами wait. notify і notifyAll. визначеними в класі Object.
Метод wait може використовуватися або з параметром, або без параметра. Цей метод переводить потік в стан очікування, в якому він буде знаходитися до тих пір, поки для потоку не буде викликаний сповіщає метод notify, notifyAll, або поки не закінчиться період часу, вказаний в параметрі методу wait.
Як користуватися методами wait, notify і notifyAll?
Метод, який буде переводитися в стан очікування, повинен бути синхронізованим, тобто його слід описати як synchronized:
У цьому прикладі всередині методу run визначено цикл, що викликає метод wait без параметрів. Кожен раз при черговому проході циклу метод run перетворюється на стан очікування до тих пір, поки інший потік не виконає повідомлення за допомогою методу notify.
Нижче ми навели приклад потоку, зухвало метод notify:
Зверніть увагу, що хоча сам метод run не синхронізовані, виклик методу notify виконується в синхронізований режимі. Як об'єкт синхронізації виступає потік, для якого викликається метод notify.
Очікування завершення потоку
За допомогою методу join ви можете виконувати очікування завершення роботи потоку. для якої цей метод викликаний.
Існує три визначення методу join:
Перший з них виконує очікування без обмеження в часі, для другого очікування буде перервано примусово через millis мілісекунд, а для третього - через millis мілісекунд і nanos наносекунд. Врахуйте, що реально ви не зможете вказувати час з точністю до наносекунд, так як дискретність системного таймера комп'ютера набагато більше.