Java. Синхронізація. Монітор. Загальні поняття. Ключове слово synchronized




Синхронізація. Монітор. Загальні поняття. Ключове слово 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
    }
  }
}

 


Зв’язані теми