Автор оригинала: Adam McQuistan.
Вступление
Эта статья является заключительным учебным пособием из серии, описывающей часто забываемые методы базового класса объектов языка Java. Ниже приведены методы базового объекта Java, которые присутствуют во всех объектах Java из-за неявного наследования объекта.
- Струна
- в класс
- равняется
- Хэш-код
- клон
- завершать
- подождите и сообщите (вы здесь)
В центре внимания этой статьи находятся методы Object#wait()
и Object#notify
(и их вариации), которые используются для связи и координации управления между потоками многопоточного приложения.
Основной обзор
Метод Object#wait()
используется в блоке синхронизации или методе-члене и заставляет вызываемый поток бесконечно ждать, пока другой поток не вызовет Object#notify()
(или его вариант Object#notifyAll()
) на том же объекте, на котором был вызван исходный Объект#wait ()
.
У ожидания есть три варианта:
void wait()
– ожидает, пока не будет вызванОбъект#notify()
илиОбъект#notifyall()
ожидание пустоты(длительный тайм - аут)
– ожидает либо истечения указанных миллисекунд, либо вызова уведомленияожидание пустоты(длительный тайм - аут, в наносекундах)
– то же, что и выше, но с дополнительной точностью в наносекундах
Объект#notify()
используется для пробуждения одного потока, ожидающего объекта, для которого был вызван wait
. Обратите внимание, что в случае нескольких потоков, ожидающих объект, пробужденный поток выбирается операционной системой случайным образом
Уведомление имеет три варианта:
void notify()
– случайным образом выбирает и пробуждает поток, ожидающий объектожидание
был вызванvoid notifyAll()
– пробуждает все потоки, ожидающие объекта
Классическая Проблема Производителя И Потребителя
Как и все в программировании, эти концепции использования Object#wait()
и Object#notify()
лучше всего понять на тщательно продуманном примере. В этом примере я собираюсь реализовать многопоточное приложение производителя/потребителя, чтобы продемонстрировать использование wait
и notify
. Это приложение будет использовать производителя для генерации случайного целого числа, которое должно представлять собой число четных случайных чисел, которые потоки-потребители должны будут генерировать случайным образом.
Дизайн класса и технические характеристики для этого примера следующие:
Производитель чисел
: создайте случайное целое число в диапазоне от 1 до 100, представляющее количество случайных четных чисел, которые потребителю потребуется сгенерировать. Случайное число должно быть помещено производителем в очередь, где потребитель может получить его и приступить к работе по получению случайных четных чисел
Очередь номеров
: очередь, которая будет ставить номер в очередь от производителя и отправлять этот номер потребителю, с нетерпением ожидающему возможности сгенерировать серию случайных четных чисел
Потребитель чисел
: потребитель, который будет извлекать число из очереди, представляющее количество случайных четных целых чисел для генерации
Очередь номеров
.
import java.util.LinkedList; public class NumberQueue { private LinkedListnumQueue = 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(); } }
Очередь номеров
имеет Связанный список
, который будет содержать внутренние данные номеров и обеспечивать доступ к ним с помощью трех синхронизированных методов. Здесь методы синхронизируются таким образом, что доступ к структуре данных LinkedList
будет заблокирован, гарантируя, что не более чем один поток может одновременно управлять методом. Кроме того, метод Очереди номеров#push-номер
вызывает свой унаследованный Объект#notifyAll
метод при добавлении нового номера, сообщающий потребителям, что работа доступна. Аналогично, метод NumberQueue#pullNumber
использует цикл вместе с вызовом унаследованного объекта#wait
метода для приостановки выполнения, если в его списке нет номеров, пока у него нет данных для потребителей.
Производитель номеров
класс.
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 миллисекунд. Это происходит до тех пор, пока очередь находится в пределах своего предела, таким образом заполняя очередь и управляя работой для потребителей.
Номер потребителя
класс.
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(); Listevens = 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); } } }
Потребитель номеров
также наследует Поток
и поддерживает ссылку на Очередь номеров
через numQueue
поле ссылки, полученное с помощью его конструктора. Его переопределенный метод запуска аналогично содержит бесконечный цикл, который внутри него извлекает число из очереди по мере их доступности. Как только он получает число, он входит в другой цикл, который производит случайные целые числа от 1 до 1000, проверяет их на четность и добавляет их в список для последующего отображения.
Как только он находит необходимое количество случайных четных чисел, указанных переменной num
, извлеченных из очереди, он выходит из внутреннего цикла и объявляет консоли о своих выводах.
Класс Бегун очереди четных чисел
.
Git Essentials
Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!
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(); } }
Бегун очереди четных чисел
является основным классом в этом приложении, который начинается с создания экземпляра класса Производитель чисел
и запускает его как поток. Затем он дает ему 3-секундную фору, чтобы заполнить свою очередь максимальным количеством четных чисел, которые должны быть сгенерированы. Наконец, класс Number Consumer
создается дважды и запускается в виде потоков, которые затем удаляются, извлекая числа из очереди и создавая указанное количество четных целых чисел.
Пример вывода из программы показан здесь. Обратите внимание, что никакие два запуска, скорее всего, не приведут к одному и тому же результату, поскольку это приложение носит чисто случайный характер, начиная с чисел, полученных случайным образом, когда операционная система переключается между активными потоками на процессоре.
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()
в Очереди номеров#push-номер
, потому что мой выбор не был случайным. Используя метод notifyAll ()
, я даю двум потокам-потребителям равные шансы вытащить номер из очереди для выполнения работы, а не оставляю ОС выбирать один из них вместо другого. Это важно, потому что если бы я просто использовал notify ()
, то есть большая вероятность, что поток, выбранный ОС для доступа к очереди, еще не готов к дополнительной работе и работает над последним набором четных чисел (хорошо, это немного неправдоподобно, что он все еще будет пытаться найти максимум 1000 четных чисел через 800 миллисекунд, но, надеюсь, вы понимаете, к чему я клоню). В принципе, я хочу здесь пояснить, что почти во всех случаях вы должны предпочесть метод notifyAll()
варианту notify ()
.
Вывод
В этой заключительной статье из серии методов класса объектов Java я рассмотрел назначение и использование вариантов wait
и notify
. Следует сказать, что эти методы довольно примитивны, и с тех пор механизмы параллелизма Java эволюционировали, но, на мой взгляд, wait
и notify
по-прежнему являются ценным набором инструментов, которые можно использовать в вашем поясе инструментов программирования Java.
Как всегда, спасибо за чтение и не стесняйтесь комментировать или критиковать ниже.