Java. Взаємодія між потоками




Взаємодія між потоками. Методи wait(), notify(), notifyAll(). Приклади

Перед вивченням даної теми рекомендується ознайомитись з наступною темою:


Зміст


Пошук на інших ресурсах:

1. Засоби взаємодії між потоками виконання. Методи wait(), notify(), notifyAll()

Для забезпечення взаємодії між потоками виконання у класі Object реалізовано 3 методи.

1. Метод wait() – переводить викликаючий потік в стан очікування. У цьому випадку викликаючий потік звільняє монітор, який має доступ до спільного ресурсу. Очікування триває до тих пір, поки інший потік виконання, який увійшов у монітор, не викличе метод notify().

Згідно з документацією Java загальна найбільш поширеної реалізації методу наступна:

public final void wait() throws java.lang.InterruptedException;

У мові Java метод wait() має інші перевантажені реалізації, які дозволяють очікувати заданий період часу.

2. Метод notify() – відновлює виконання потоку, з якого був викликаний метод wait() для того самого об’єкту.

Згідно з документацією Java загальна форма методу наступна:

public final native void notify();

3. Метод notifyAll() – відновлює виконання всіх потоків, з яких був викликаний метод wait() для того самого об’єкту. Управління передається одному з цих потоків.

Загальна форма методу наступна:

public final native void notifyAll();

 

2. Схема (рисунок) взаємодії між двома паралельними потоками

На рисунку зображено взаємодію між двома потоками, які виконуються паралельно. Потоки обмінюються екземпляром класу Value, що зберігає деяке значення. Кожен потік представлений класом. Клас, що формує екземпляр Value називається PutValue. Клас, що зчитує екземпляр Value має назву GetValue.

Спочатку формується (записується) деяке значення екземпляру Value потоком PutValue. У цей момент потік GetValue очікує повідомлення від потоку PutValue з допомогою циклічного виклику методу wait(). Після того, як екземпляр класу Value сформовано, потік PutValue викликає метод notify(). У результаті управління передається потоку GetValue який повинен зчитати значення. Тепер вже потік PutValue очікує (метод wait()). Як тільки потік GetValue завершить зчитування, він повідомляє про це шляхом виклику методу notify().

Процес синхронізованої взаємодії між потоками може повторюватись скільки завгодно.

Java. Потоки виконання. Взаємодія між паралельними потоками

Рисунок 1. Взаємодія між паралельними потоками PutValue та GetValue, які обмінюються спільним ресурсом Value

 

3. Реалізація системи опитування між потоками виконання. Приклад обміну значеннями між двома потоками, які виконуються паралельно

Розв’язок даного прикладу можна використовувати як шаблон для реалізації взаємодії між власно розробленими потоками.

Умова задачі. Реалізувати обмін значенням між двома потоками, які виконуються паралельно. Перший потік формує деяке випадкове число. Другий потік очікує, поки число не буде сформоване першим потоком. Потім це число передається у другий потік і там виводиться. Після цього управління знову передається першому потоку для формування наступного випадкового числа. Процес повторюється задану кількість разів.

Розв’язок. Для розв’язку даної задачі потрібно сформувати 3 класи:

  • клас Number, який визначає число яке використовується для обміну між потоками;
  • клас FormNumber – представляє потік, що формує випадкові числа, які будуть передаватись у потік GetNumber;
  • клас GetNumber – представляє потік, що отримує числа з потоку FormNumber.

 

// Клас, що реалізує методи, які синхронізовано отримують та повертають число
class Number {
  int n;

  // прапорець, що визначає чи паралельний потік
  // сформував/отримав число
  boolean valueSet = false;

  // Метод, що повертає число
  synchronized int get() {
    while (!valueSet) {
      try {
        wait();
      }
      catch (InterruptedException e) {
        System.out.println(e.getMessage());
      }
    }

    valueSet=false;
    notify();
    return n;
  }

  // Метод, що використовується для передачі числа іншому потоку
  synchronized void put(int n) {

    // Очікувати, поки не буде отримано (valueSet=false)
    while(valueSet) {
      try {
        wait();
      }
      catch(InterruptedException e) {
        System.out.println(e.getMessage());
      }
    }

    this.n = n;
    valueSet=true;
    notify();
  }
}

// Клас, що представляє потік, який формує число
class FormNumber implements Runnable {
  Number num; // Число, що формується
  int count; // кількість чисел, які потрібно відправити іншому потоку
  Thread thread; // Посилання на поточний потік

  // Конструктор - отримує число, та кількість сформованих чисел
  FormNumber(Number num, int count) {
    // Заповнити внутрішні змінні
    this.num = num;
    this.count = count;

    // Створити потік і запустити його на виконання
    thread = new Thread(this, "FormNumber");
    thread.start();
  }

  // Метод, що реалізує код виконання потоку
  public void run() {
    for (int i=0; i<count; i++) {
      try {
        Thread.sleep(200);
      } 
      catch (InterruptedException e) {
        e.printStackTrace();
      }

      // Сформувати деяке випадкове число від 1 до 100
      int number = (int)(Math.random()*100+1);
      System.out.println("Потік: " + thread.getName() + ", сформовано число: " + number);
      num.put(number);
    }
  }
}

class GetNumber implements Runnable {
  // Внутрішні змінні
  Number num; // число, яке формується
  int count; // кількість чисел, які потрібно отримати
  Thread thread; // посилання на поточний потік

  GetNumber(Number num, int count) {
    // Заповнити внутрішні змінні
    this.num = num;
    this.count = count;

    // Створити потік та запустити його на виконання
    thread = new Thread(this, "GetNumber");
    thread.start();
  }

  // Метод, що містить код виконання потоку
  public void run() {
    for (int i=0; i<count; i++) {
      try {
        Thread.sleep(200);
      } 
      catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("Потік: " + thread.getName() + ", отримано число " + num.get());
    }
  }
}

public class Threads {

  public static void main(String[] args) {
    // Тест роботи потоків
    Number num = new Number();
    new FormNumber(num, 5); // Передати в інший потік 5 чисел
    new GetNumber(num, 5); // Отримати з іншого потоку 5 чисел
  }
}

Результат виконання програми

Потік: FormNumber, сформовано число: 26
Потік: GetNumber, отримано число 26
Потік: FormNumber, сформовано число: 10
Потік: GetNumber, отримано число 10
Потік: FormNumber, сформовано число: 88
Потік: GetNumber, отримано число 88
Потік: FormNumber, сформовано число: 58
Потік: GetNumber, отримано число 58
Потік: FormNumber, сформовано число: 70
Потік: GetNumber, отримано число 70

 

4. Приклад почергового синхронізованого використання спільного масиву чисел між потоками. Реалізація системи опитування

Шаблон даного прикладу можна використовувати для реалізації власних

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

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

Розв’язок. Для розв’язку задачі потрібно реалізувати 3 класи:

  • Array – визначає масив цілих чисел заданого розміру;
  • FormArray – призначений для формування масиву випадкових цілих чисел довільного розміру;
  • GetArray – використовується для отримання масиву чисел, попередньо згенерованих в класі FormArray.

У класі Array реалізовано такі члени класу:

  • AI – масив цілих чисел. Цей масив є спільним ресурсом для потоків, яким відповідають класи FormArray та GetArray;
  • formArray – прапорець типу boolean, який використовується для визначення потоками періоду очікування;
  • конструктор Array(), який отримує параметром розмір масиву. У конструкторі виділяється пам’ять для масиву AI;
  • метод get() – використовується потоком GetArray для зчитування масиву AI;
  • метод set() – використовується потоком FormArray для формування масиву цілих чисел;
  • метод getArray() – посилання на прихований (private) масив AI.

Обидва класи FormArray та GetArray реалізують інтерфейс Runnable. Класи інкапсулюють потоки виконання. У класах реалізовано наступні складові елементи:

  • A – екземпляр (об’єкт) класу Array. Цей екземпляр зберігає масив цілих чисел;
  • count – цілочисельне значення, яке визначає кількість ітерацій при виконанні кожної з яких буде сформовано новий масив;
  • конструктор;
  • метод run(), який містить код потоку виконання. Для класу FormArray цей метод містить код формування масиву цілих чисел з допомогою виклику методу put() екземпляру A. У методі run() класу GetArray викликається метод get() екземпляру A для читання масиву, сформованого у паралельному потоці FormArray.

 

// Клас, що реалізує масив з синхронізованими методами get(), set().
class Array {
  // Приховані внутрішні поля класу
  private int[] AI;
  private boolean formArray;

  // Конструктор - отримує ззовні розмір масиву
  Array(int size) {
    // Сформувати масив
    AI = new int[size];
    formArray = false;
  }

  // Метод синхронізованого читання масиву.
  synchronized int[] get() {
    // очікувати, поки не буде сформовано масив
    while (!formArray) {
      try {
        wait();
      }
      catch (InterruptedException e) {
        System.out.println(e.getMessage());
      }
    }

    // Змінити прапорець взаємодії
    formArray = false;

    // Повідомити інший потік, що масив отримано
    notify();

    // Повернути результат
    return AI;
  }

  // Метод синхронізованого запису масиву чисел
  synchronized void put(int[] _AI) {
    // Очікувати, поки не буде прочитано масив (formArray=false)
    while(formArray) {
      try {
        wait();
      }
      catch(InterruptedException e) {
        System.out.println(e.getMessage());
      }
    }

    // Записати масив у спільний ресурс
    AI = _AI;

    // Змінити прапорець взаємодії між потоками
    formArray = true;

    // Повідомити інший потік, що масив сформовано
    notify();
  }

    // Метод доступу до масиву AI
    int[] getArray() { return AI; }
  }

// Клас, що реалізує потік, який формує масив випадкових чисел.
class FormArray implements Runnable {
  // Внутрішні поля класу
  private Array A; // власне масив
  private int count; // кількість ітерацій формування масиву

  // Конструктор.
  // Параметри:
  // - A - посилання на спільний масив;
  // - count - кількість ітерацій, що формують масив.
  FormArray(Array A, int count) {
    // Заповнити внутрішні змінні
    this.A = A;
    this.count = count;

    // Створити потік
    new Thread(this, "FormArray").start();
  }

  // Безпосереднє виконання потоку
  public void run() {
    // count - кількість ітерацій, що формують масив
    for (int k = 0; k<count; k++) {
      try {
        // Затримка на 1 секунду
        Thread.sleep(1000);
      } 
      catch (InterruptedException e) {
        e.printStackTrace();
      }

      // сформувати випадковий масив з випадковими числами
      int[] AI = new int[A.getArray().length];

      // записати в масив випадкові числа
      for (int i=0; i<AI.length; i++)
        AI[i] = (int)(Math.random()*AI.length+1);

      // Вивести сформований масив
      System.out.println("Сформовано: ");
      for (int i=0; i<AI.length; i++)
        System.out.print(AI[i] + " ");
      System.out.println();

      // записати масив у спільний синхронізований ресурс
      A.put(AI);
    }
  }
}

// Клас, що реалізує потік в якому зчитується масив цілих чисел.
// Масив цілих чисел попередньо сформований в екземплярі класу FormArray
class GetArray implements Runnable {
  // Внутрішні члени даних класу
  private Array A;
  private int count;

  // Конструктор. Отримує 2 параметри:
  // - _A - масив, що є спільним ресурсом для двох потоків;
  // - count - кількість ітерацій (кроків) зчитування масиву з паралельного потоку.
  GetArray(Array _A, int count) {
    // Заповнити внутрішні змінні
    A = _A;
    this.count = count;
    new Thread(this, "GetArray").start();
  }

  // Код виконання потоку
  public void run() {
    for (int k=0; k<count; k++) {
      try {
        // Затримка на 0.2 секунди
        Thread.sleep(200);
      }
      catch (InterruptedException e) {
        e.printStackTrace();
      }

      // Отримати масив чисел з синхронізованого потоку
      int[] AI = A.get();

      // Вивести отриманий масив
      System.out.println("Отримано: ");
      for (int i=0; i<AI.length; i++)
        System.out.print(AI[i] + " ");
      System.out.println();
    }
  }
}

public class Threads {

  public static void main(String[] args) {
    Array A = new Array(10);
    new FormArray(A, 6);
    new GetArray(A, 6);
  }
}

 


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