Стратегия «wait-notify-notifyAll» - 1

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

Хочу основательно разобрать с тобой тему 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 веке.