Синхронізація. Монітор. Загальні поняття. Ключове слово synchronized
Зміст
- 1. Поняття синхронізації між потоками. Необхідність застосування синхронізації. Монітор
- 2. Модифікатор доступу synchronized. Загальна форма
- 3. Оператор synchronized() { }. Загальна форма
- 4. Приклад, що демонструє синхронізований доступ до спільного методу з трьох різних потоків. Застосування модифікатора доступу synchronized
- 5. Приклад застосування оператора synchronized() {} для синхронізованого доступу до спільного ресурсу
- Зв’язані теми
Пошук на інших ресурсах:
1. Поняття синхронізації між потоками. Необхідність застосування синхронізації. Монітор
Бувають випадки, коли два або більше паралельно-виконуваних потоків пробують доступитись до спільного ресурсу. Якщо ресурс може бути змінений в результаті виконання одного з потоків, то інші потоки повинні дочекатись поки зміни будуть завершені. В противному випадку, потоки отримають ресурс, дані якого будуть помилковими.
Гарантувати одночасне використання спільного ресурсу тільки одним потоком може так звана синхронізація. Отже, синхронізація – це процес, який впорядковує доступ з різних потоків до спільного ресурсу.
Синхронізація базується на використанні моніторів. Монітор – це об’єкт, який використовується для взаємовиключного блокування (взаимоисключающей блокировки). Взаємовиключне блокування дозволяє володіти монітором тільки одному об’єкту, що є потоком. Кожен об’єкт, що є потоком, має власний, неявно зв’язаний з ним монітор.
Потік виконання (який представлений об’єктом) може заволодіти монітором у випадку, якщо він запросив блокування і монітор вільний на даний момент. Після того, як об’єкт увійшов у монітор всі інші об’єкти-потоки, що пробують увійти в монітор, призупиняються і очікують до тих пір, поки перший об’єкт не вийде з монітору.
Монітором може володіти тільки один потік. Якщо потік (об’єкт) володіє монітором, то він за необхідності може повторно увійти в нього.
У мові Java синхоронізація застосовується до цілих методів або фрагментів кодів. Виходячи з цього існує два способи синхронізації програмного коду:
- з допомогою використання модифікатора доступу synchronized;
- з допомогою використання оператора synchronized() {}.
⇑
2. Модифікатор доступу synchronized. Загальна форма
Модифікатор доступу synchronized застосовується при оголошені синхронізованого методу і має наступну загальну форму:
synchronized return_type MethodName(parameters) {
...
}
тут
- MethodName – ім’я методу, який є спільним ресурсом і його потрібно синхронізувати;
- return_type -тип, що повертає метод;
- parameters – параметри методу.
⇑
3. Оператор synchronized() { }. Загальна форма
Оператор synchronized(), на відміну від модифікатора доступу synchronized, використовується для застосування синхронізації до об’єктів (методів), які першопочатково при їх розробці не призначались для багатопотокового доступу. Тобто, в класі не реалізовано методів з модифікатором доступу synchronized (класи сторонніх розробників).
Загальна форма оператора synchronized() наступна:
synchronized (reference) { // Оператори, які потрібно синхронізувати // ... }
тут
- reference – посилання на об’єкт, який синхронізується;
- фігурні дужки { } є блоком синхронізації. У цьому блоці вказуються оператори, які потрібно синхронізувати.
⇑
4. Приклад, що демонструє синхронізований доступ до спільного методу з трьох різних потоків. Застосування модифікатора доступу synchronized
У прикладі демонструється необхідність застосування модифікатора доступу synchronized з метою впорядкування доступу до спільного ресурсу з різних потоків.
Спільним ресурсом є метод Get() класу Array5. Метод Get() повертає масив з 5 чисел, значення яких послідовно змінюються від 1 до 5. До цього методу буде здійснена спроба одночасного доступу з різних потоків, тому метод позначений як synchronized.
Інкапсуляція одного потоку виконання здійснюється в класі ArrayThread класичним (стандартним) способом з допомогою реалізації інтерфейсу Runnable. Клас ArrayThread містить наступні загальнодоступні (public) елементи:
- A – посилання на екземпляр класу Array5. З допомогою цього посилання відбувається доступ до методу Get();
- AI – масив чисел, який отримується з методу Get();
- t – посилання на поточний потік;
- конструктор ArrayThread(), який отримує екземпляр класу Array5 та назву потоку. У конструкторі відбувається створення потоку виконання та його запуск;
- метод run(), який викликає метод Get() з допомогою посилання A.
У класі Threads представлено функцію main(), в якій створюється три потоки AT1, AT2, AT3 типу ArrayThread та один об’єкт A5. Метод Get() об’єкту A5 є спільним ресурсом для цих трьох потоків.
// Тема. Синхронізація потоків. // Клас, що містить спільний ресурс class Array5 { // Синхронізований метод, що повертає масив з 5 чисел // і виводить його на екран synchronized public int[] Get(String threadName) { // Вивести назву потоку, який виконується System.out.println("Begin => " + threadName); // Сформувати 5 цілих чисел від 1 до 5 int[] AI = new int[5]; for (int i=0; i<AI.length; i++) AI[i] = i+1; // Викликати затримку,щоб інші потоки могли виконуватись try { Thread.sleep(100); } catch (InterruptedException e) { System.out.println(e.getMessage()); } // Вивести в методі масив AI для звірки System.out.print(threadName + " = [ "); for (int i=0; i<AI.length; i++) System.out.print(AI[i] + " "); System.out.println(" ]"); // Сповістити про закінчення виконання методу System.out.println("End => " + threadName); return AI; } } // Клас, який є потоком, що читає масив чисел class ArrayThread implements Runnable { Array5 A; // екземпляр спільного ресурсу int[] AI; // масив з 5 чисел, що отримується зі спільного ресурсу Thread t; // посилання на поточний потік // Конструктор - отримує параметри: // - ім'я потоку; // - екземпляр класу Array5. ArrayThread(String threadName, Array5 A) { // Зберегти масив this.A = A; // Створити потік t = new Thread(this, threadName); // Запустити потік на виконання t.start(); // виклик методу run() } // Метод, в якому запускається потік, що створює масив з 5 чисел public void run() { AI = A.Get(t.getName()); // отримати масив чисел від 1 до 5 } } public class Threads { public static void main(String[] args) { // Продемонструвати доступ до спільного ресурсу з 3-х різних потоків // 1. Оголосити екземпляр, який буде спільним ресурсом у трьох різних потоках Array5 A5 = new Array5(); // 2. Оголосити три різні потоки і запустити їх ArrayThread AT1 = new ArrayThread("AT1", A5); ArrayThread AT2 = new ArrayThread("AT2", A5); ArrayThread AT3 = new ArrayThread("AT3", A5); // 3. Дочекатись завершення усіх трьох потоків try { AT1.t.join(); AT2.t.join(); AT3.t.join(); } catch (InterruptedException e) { System.out.println(e.getMessage()); } } }
Результат виконання програми
Begin => AT1 AT1 = [ 1 2 3 4 5 ] End => AT1 Begin => AT3 AT3 = [ 1 2 3 4 5 ] End => AT3 Begin => AT2 AT2 = [ 1 2 3 4 5 ] End => AT2
Якщо у вищенаведеному прикладі перед методом Get() класу Array5 забрати ключове слово synchronized
class Array5 { ... // тут немає модифікатора synchronized public int[] Get(String threadName) { ... } }
то послідовного виконання потоків не буде. У цьому випадку програма після кожного запуску буде видавати різний (хаотичний) результат, наприклад такий
Begin => AT1 Begin => AT2 Begin => AT3 AT1 = [ AT2 = [ 1 1 2 2 3 3 4 4 5 ] End => AT1 5 ] End => AT2 AT3 = [ 1 2 3 4 5 ] End => AT3
⇑
5. Приклад застосування оператора synchronized() {} для синхронізованого доступу до спільного ресурсу
Код попереднього прикладу (дивіться п. 4), що синхронізує потоки, можна представити по іншому. У цьому випадку, замість модифікатора доступу synchronized перед іменем методу Get() потрібно використати оператор synchronized() { }. Оператор synchronized повинен бути використаний у методі run() класу ArrayThread.
Після внесених змін, скорочений код програми буде наступний:
// Клас, що є спільним ресурсом class Array5 { // Метод, що повертає масив з 5 чисел і виводить його на екран. // Вже не синхронізований public int[] Get(String threadName) { ... } } // Клас, який є потоком, що читає масив чисел class ArrayThread implements Runnable { Array5 A; // екземпляр спільного ресурсу int[] AI; // масив з 5 чисел, що отримується зі спільного ресурсу Thread t; // посилання на поточний потік // Конструктор - отримує параметри: // - ім'я потоку; // - екземпляр класу Array5. ArrayThread(String threadName, Array5 A) { ... } // Метод, в якому запускається потік, що створює масив з 5 чисел public void run() { // Оператор synchronized для спільного ресурсу A synchronized (A) { AI = A.Get(t.getName()); // отримати масив чисел від 1 до 5 } } }
⇑
Зв’язані теми
- Багатозадачність. Потоки виконання. Основні поняття
- Засоби Java для роботи з потоками виконання. Клас Thread. Інтерфейс Runnable. Головний потік. Створення дочірнього потоку
- Методи класу Thread: getName(), run(), start(), sleep(). Приклади
- Методи класу Thread: join(), isAlive(), getPriority(), setPriority(). Приклади
- Взаємодія між потоками. Методи wait(), notify(), notifyAll(). Приклади
- Стани потоку виконання. Метод getState(). Приклад
⇑