— Привіт, Аміго!

Хочу як слід розібрати з тобою тему wait-notify. Методи wait-notify забезпечують зручний механізм взаємодії потоків. Також їх можна використовувати для побудови складних високорівневих механізмів взаємодії потоків.

Почнімо з прикладу. Уявімо, що у нас є програма для сервера, яка має виконувати різні задачі, які користувачі додають через сайт. Користувачі додають різні задачі в різний час. Задачі ресурсомісткі, але сервер у нас із восьмиядерним процесором — впорається. Як виконувати задачі на сервері?

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

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

— А можна приклад такого об'єкта-задачі?

— Ось дивися:

Клас обчислює факторіал числа n після виклику методу run()
class Factorial implements Runnable
{
 public int n = 0;
 public long result = 1;

 public Factorial (int n)
 {
  this.n = n;
 }

 public void run()
 {
  for (int i=2;i<=n;i++)
   result*=i;
 }
}

— Поки що все зрозуміло.

— Чудово. У такому разі розберемо, який вигляд має бути в об'єкта-черги. Що ти можеш про нього сказати?

— Він має бути thread-safe. В нього розміщуються об'єкти-задачі (таски) потоком, який приймає їх від користувачів, а забираються задачі потоками-виконавцями.

— Ага. А якщо задачі тимчасово вичерпалися?

— Тоді потоки-виконавці повинні чекати, поки вони з'являться.

— Саме так. Тоді уяви, що все це можна вбудувати в одну чергу. Ось поглянь-но:

Черга задач, якщо задач нема, — потік засинає та чекає на їхню появу:
public class JobQueue
{
 ArrayList jobs = new ArrayList();

 public synchronized void put(Runnable job)
 {
  jobs.add(job);
  this.notifyAll();
 }

 public synchronized Runnable getJob()
 {
  while (jobs.size()==0)
   this.wait();

  return jobs.remove(0);
 }
}

У нас є метод getJob, який спостерігає: якщо список роботи (jobs) пустий, потік засинає (wait), допоки в списку щось не з'явиться.

А ще є метод put, який дозволяє додати до списку jobs нову задачу (job). Як тільки додано нову задачу, викликається метод notifyAll. Виклик цього методу розбудить усі потоки-виконавці, які заснули всередині методу getJob.

— А можеш нагадати ще раз, як працюють методи wait і notify?

— Метод wait викликається лише всередині блоку synchronized, в об'єкта-м'ютекса. У нашому випадку це this. У такому разі відбуваються дві речі:

1) Потік засинає.

2)Потік тимчасово звільняє м'ютекс (поки не прокинеться).

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

Метод notifyAll теж можна викликати лише всередині блоку synchronized в об'єкта-м'ютекса. У нашому випадку це this. При цьому відбуваються дві речі:

1) Прокидаються всі потоки, які заснули на цьому об'єкті-м'ютексі.

2) Як тільки нинішній потік вийде з блоку synchronized, один з потоків, що прокинувся, захопить м'ютекс та продовжить працювати. Коли він звільнить м'ютекс, інший потік, що прокинувся, захопить м'ютекс, тощо.

Дуже схоже на автобус. Ви заходите всередину, хочете передати гроші за проїзд, а водія нема. І ви «засинаєте». З часом набивається повний автобус пасажирів, але за проїзд поки що ніхто не передає – нема кому. Потім заходить водій і ви чуєте: «Передаємо за проїзд!». И тут починається…

— Цікаве порівняння. А що таке автобус?

— Ну, були такі дивні штуки в 21 столітті.