Методы объектов Java: ждать и уведомлять

Введение Эта статья является заключительным учебным курсом из серии, описывающей методы, о которых часто забывают, базового класса Object языка Java. Ниже приведены методы базового объекта Java, которые присутствуют во всех объектах Java из-за неявного наследования объекта. * toString [/ javas-object-methods-tostring /] * toClass [/ javas-object-methods-getclass /] * равно [/ javas-object-methods-equals-object /] * hashCode [/ javas-object-methods -hashcode /] * клонировать [/ javas-object-methods

Вступление

Эта статья является заключительным учебным курсом из серии, описывающей часто забываемые методы базового класса 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.

Как всегда, спасибо за чтение и не стесняйтесь комментировать или критиковать ниже.

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus