Вступление
Эта статья является заключительным учебным курсом из серии, описывающей часто забываемые методы базового класса Object языка Java. Ниже приведены методы базового объекта Java, которые присутствуют во всех объектах Java из-за неявного наследования объекта.
- нанизывать
- для класса
- равно
- хэш-код
- клон
- завершить
- подожди и уведомь (ты здесь)
В центре внимания этой статьи находятся Object#wait()
и
Object#notify
(и их варианты), которые используются для связи и
координации управления между потоками многопоточного приложения.
Базовый обзор
Object#wait()
метод используется в блоке синхронизации или метода
члена и вызывает поток его называют в ждать неопределенное время, пока
другой поток не вызовет Object#notify()
(или его вариации
Object#notifyAll()
) на том же объекте что был вызван Object#wait()
У Wait есть три варианта:
void wait()
- ждет , пока либоObject#notify()
илиObject#noifyAll()
называетсяvoid wait(long timeout)
- ожидает истечения указанных миллисекунд или вызывается уведомлениеvoid wait(long timeout, int nanos)
- то же самое, что и выше, но с дополнительной точностью предоставленных наносекунд
Object#notify()
используется , чтобы проснуться в один поток,
ожидающий на объекте , который wait
был вызван. Обратите внимание, что
в случае нескольких потоков, ожидающих на объекте, активизированный
поток выбирается операционной системой случайным образом.
Уведомление имеет три варианта:
void notify()
- случайным образом выбирает и пробуждает поток, ожидающий, когда объектwait
был вызванvoid notifyAll()
- будит все потоки, ожидающие объекта
Проблема потребителей классического производителя
Как и все в программировании, эти концепции использования
Object#wait()
и Object#notify()
лучше всего понять на тщательно
продуманном примере. В этом примере я собираюсь реализовать
многопоточное приложение производителя / потребителя, чтобы
продемонстрировать использование wait
и notify
. Это приложение
будет использовать производителя для генерации случайного целого числа,
которое должно представлять количество четных случайных чисел, которые
потоки-потребители должны будут генерировать случайным образом.
Дизайн класса и спецификации для этого примера следующие:
NumberProducer
: создает случайное целое число от 1 до 100,
которое представляет количество случайных четных чисел, которые
потребителю необходимо будет сгенерировать. Случайное число должно быть
помещено производителем в очередь, где потребитель может получить его и
приступить к работе, производя случайные четные числа.
NumberQueue
: очередь, которая ставит номер в очередь от
производителя и выводит его из очереди для потребителя, с нетерпением
ожидающего возможности сгенерировать серию случайных четных чисел.
NumberConsumer
: потребитель, который будет извлекать из очереди
число, представляющее количество случайных четных целых чисел для
генерации.
NumberQueue
.
import java.util.LinkedList;
public class NumberQueue {
private LinkedList<Integer> numQueue = new LinkedList<>();
public synchronized void pushNumber(int num) {
numQueue.addLast(num);
notifyAll();
}
public synchronized int pullNumber() {
while(numQueue.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return numQueue.removeFirst();
}
public synchronized int size() {
return numQueue.size();
}
}
NumberQueue
имеет LinkedList
который будет содержать данные чисел
внутри и предоставлять доступ к ним с помощью трех синхронизированных
методов. Здесь методы синхронизируются, поэтому на доступ к LinkedList
будет установлена блокировка, гарантирующая, что только один поток может
контролировать метод одновременно. Кроме того, метод
NumberQueue#pushNumber
вызывает унаследованный Object#notifyAll
при
добавлении нового числа, позволяя потребителям узнать, что работа
доступна. Точно так же метод NumberQueue#pullNumber
использует цикл
вместе с вызовом унаследованного Object#wait
для приостановки
выполнения, если в его списке нет номеров, пока не будут получены данные
для потребителей.
Класс NumberProducer
.
import java.util.Random;
public class NumberProducer extends Thread {
private int maxNumsInQueue;
private NumberQueue numsQueue;
public NumberProducer(int maxNumsInQueue, NumberQueue numsQueue) {
this.maxNumsInQueue = maxNumsInQueue;
this.numsQueue = numsQueue;
}
@Override
public void run() {
System.out.println(getName() + " starting to produce ...");
Random rand = new Random();
// continuously produce numbers for queue
while(true) {
if (numsQueue.size() < maxNumsInQueue) {
// random numbers 1-100
int evenNums = rand.nextInt(99) + 1;
numsQueue.pushNumber(evenNums);
System.out.println(getName() + " adding " + evenNums);
}
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
NumberProducer
наследует Thread
и содержит поле с именем
maxNumsInQueue
которое устанавливает ограничение на количество
элементов, которые может содержать очередь, а также имеет ссылку на
NumberQueue
через его numsQueue
, которое он получает с помощью
одного конструктора. Он переопределяет метод Thread#run
который
содержит бесконечный цикл, который добавляет случайное целое число от 1
до 100 в NumberQueue
каждые 800 миллисекунд. Это происходит, пока
очередь находится в пределах своего лимита, таким образом заполняя
очередь и управляя работой для потребителей.
Класс NumberConsumer
.
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.StringJoiner;
public class NumberConsumer extends Thread {
private NumberQueue numQueue;
public NumberConsumer(NumberQueue numQueue) {
this.numQueue = numQueue;
}
@Override
public void run() {
System.out.println(getName() + " starting to consume ...");
Random rand = new Random();
// consume forever
while(true) {
int num = numQueue.pullNumber();
List<Integer> evens = new ArrayList();
while(evens.size() < num) {
int randInt = rand.nextInt(999) + 1;
if (randInt % 2 == 0) {
evens.add(randInt);
}
}
String s = " " + getName() + " found " + num + " evens [";
StringJoiner nums = new StringJoiner(",");
for (int randInt : evens) {
nums.add(Integer.toString(randInt));
}
s += nums.toString() + "]";
System.out.println(s);
}
}
}
NumberConsumer
также наследуется от Thread
и поддерживает ссылку на
NumberQueue
через numQueue
полученное через его конструктор. Его
переопределенный метод запуска также содержит бесконечный цикл, который
внутри него извлекает число из очереди по мере их доступности. Как
только он получает число, он входит в другой цикл, который производит
случайные целые числа от 1 до 1000, проверяет его на равномерность и
добавляет их в список для последующего отображения.
Как только он находит необходимое количество случайных четных чисел,
заданных num
извлеченной из очереди, он выходит из внутреннего цикла и
объявляет на консоль свои результаты.
Класс EvenNumberQueueRunner
.
public class EvenNumberQueueRunner {
public static void main(String[] args) {
final int MAX_QUEUE_SIZE = 5;
NumberQueue queue = new NumberQueue();
System.out.println(" NumberProducer thread NumberConsumer threads");
System.out.println("============================= =============================");
NumberProducer producer = new NumberProducer(MAX_QUEUE_SIZE, queue);
producer.start();
// give producer a head start
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
NumberConsumer consumer1 = new NumberConsumer(queue);
consumer1.start();
NumberConsumer consumer2 = new NumberConsumer(queue);
consumer2.start();
}
}
EvenNumberQueueRunner
- это основной класс в этом приложении, который
начинается с создания экземпляра NumberProducer
и запускает его как
поток. Затем он дает ему 3-секундную фору, чтобы заполнить очередь
максимальным количеством четных чисел, которые необходимо сгенерировать.
Наконец, NumberConsumer
дважды и запускается как потоки, которые затем
извлекают числа из очереди и создают указанное количество четных целых
чисел.
Здесь показан пример вывода программы. Обратите внимание, что никакие два запуска, скорее всего, не приведут к одинаковому результату, поскольку это приложение является чисто случайным по своей природе от полученных чисел до случайности переключения операционной системы между активными потоками на ЦП.
NumberProducer thread NumberConsumer threads
============================= =============================
Thread-0 starting to produce ...
Thread-0 adding 8
Thread-0 adding 52
Thread-0 adding 79
Thread-0 adding 62
Thread-1 starting to consume ...
Thread-2 starting to consume ...
Thread-1 found 8 evens [890,764,366,20,656,614,86,884]
Thread-2 found 52 evens [462,858,266,190,764,686,36,730,628,916,444,370,860,732,188,652,274,608,912,940,708,542,760,194,642,192,22,36,622,174,66,168,264,472,228,972,18,486,714,244,214,836,206,342,388,832,8,666,946,116,342,62]
Thread-2 found 62 evens [404,378,276,308,470,156,96,174,160,704,44,12,934,426,616,318,942,320,798,696,494,484,856,496,886,828,386,80,350,920,142,686,118,240,398,488,976,512,642,108,542,122,536,482,734,430,564,200,844,462,12,124,368,764,496,728,802,836,478,986,292,486]
Thread-1 found 79 evens [910,722,352,656,250,974,602,342,144,952,916,188,286,468,618,496,764,642,506,168,966,274,476,744,142,348,784,164,346,344,48,862,754,896,896,784,574,464,134,192,446,524,424,710,128,756,934,672,816,604,186,18,432,250,466,144,930,914,670,434,764,176,388,534,448,476,598,984,536,920,282,478,754,750,994,60,466,382,208]
Thread-0 adding 73
Thread-2 found 73 evens [798,692,698,280,688,174,528,632,528,278,80,746,790,456,352,280,574,686,392,26,994,144,166,806,750,354,586,140,204,144,664,214,808,214,218,414,230,364,986,736,844,834,826,564,260,684,348,76,390,294,740,550,310,364,460,816,650,358,206,892,264,890,830,206,976,362,564,26,894,764,726,782,122]
Thread-0 adding 29
Thread-1 found 29 evens [274,600,518,222,762,494,754,194,128,354,900,226,120,904,206,838,258,468,114,622,534,122,178,24,332,432,966,712,104]
Thread-0 adding 65
... and on and on ...
Я хотел бы воспользоваться моментом, чтобы объяснить свое использование
метода notifyAll()
в NumberQueue#pushNumber
потому что мой выбор не
был случайным. Используя метод notifyAll()
я даю двум потребительским
потокам равные шансы вытащить число из очереди для выполнения работы, а
не оставлять его на усмотрение ОС, чтобы выбрать один из них. Это важно,
потому что, если бы я просто использовал notify()
велика вероятность,
что поток, который ОС выбирает для доступа к очереди, еще не готов для
выполнения дополнительной работы и работает над своим последним набором
четных чисел (хорошо, его немного надумано, что он все равно будет
пытаться найти максимум 1000 четных чисел через 800 миллисекунд, но,
надеюсь, вы понимаете, к чему я клоню). В основном то , что я хочу ясно
дать понять , что здесь почти во всех случаях следует отдавать
предпочтение notifyAll()
метод над notify()
вариант.
Заключение
В этой заключительной статье серии методов класса Java Object я
рассмотрел назначение и использование вариантов wait
и notify
.
Следует сказать, что эти методы довольно примитивны, и с тех пор
механизмы параллелизма Java эволюционировали, но, на мой взгляд, метод
wait
и notify
прежнему является ценным набором инструментов, который
должен быть в вашем поясе инструментов программирования Java.
Как всегда, спасибо за чтение и не стесняйтесь комментировать или критиковать ниже.