В предыдущей статье я представил метод использования Java8 для реализации шаблона наблюдателя (часть 1). Эта статья продолжает представлять соответствующие знания о образце наблюдателя Java8. Конкретный контент заключается в следующем:
Реализация защиты потока
Предыдущая глава представляет реализацию образца наблюдателя в современной среде Java. Хотя это просто, но полное, эта реализация игнорирует вопрос о ключе: безопасность потока. Большинство открытых Java-приложений являются многопоточными, а режим наблюдателя в основном используется в многопоточных или асинхронных системах. Например, если внешняя служба обновляет свою базу данных, приложение также будет получать сообщение асинхронно, а затем уведомит внутренний компонент об обновлении в режиме Observer, а не напрямую регистрирует и прослушивает внешнюю службу.
Безопасность потока в режиме наблюдателя в основном сосредоточена на теле режима, потому что конфликты потоков, вероятно, возникнут при изменении зарегистрированной коллекции слушателей. Например, один поток пытается добавить нового слушателя, в то время как другой поток пытается добавить новый объект животного, который запускает уведомления всем зарегистрированным слушателям. Учитывая порядок последовательности, первый поток может или не может завершить регистрацию нового слушателя, прежде чем зарегистрированный слушатель получит уведомление о добавленном животном. Это классический случай соревнования по ресурсам потоков, и именно это явление сообщает разработчикам, что им нужен механизм для обеспечения безопасности потока.
Самое простое решение этой проблемы: все операции, которые получают доступ или изменяют список регистрационных слушателей, должны следовать механизму синхронизации Java, такие как:
Общедоступный синхронизированный AnimalAddedListener RegisterAnimaladdEdlistener (AnimalAddedLister Slister) {/*....*//} Public Synchronized void ungisteranimaladdedlistener (AnimalAddedListener) {/*...Таким образом, в то же время только один поток может изменить или получить доступ к зарегистрированному списку слушателей, который может успешно избежать проблем соревнований по ресурсам, но возникают новые проблемы, и такие ограничения слишком строгие (для получения дополнительной информации о синхронизированных ключевых словах и моделях параллели Java, пожалуйста, обратитесь к официальной веб -странице). Благодаря синхронизации метода, одновременный доступ к списку слушателей может наблюдаться всегда. Регистрация и отзыв слушателя-это операция записи для списка слушателей, в то время как уведомление слушателя о доступе к списку слушателя-это операция только для чтения. Поскольку доступ через уведомление является операцией чтения, многочисленные операции уведомлений могут быть выполнены одновременно.
Следовательно, до тех пор, пока нет никакой регистрации или отзывы слушателя, если регистрация не зарегистрирована, до тех пор, пока любое количество параллельных уведомлений может быть выполнено одновременно без запуска конкуренции за ресурсы для списка зарегистрированного слушателя. Конечно, конкурс ресурсов в других ситуациях существовал в течение длительного времени. Чтобы решить эту проблему, блокировка ресурсов для ReadWritelock предназначена для управления операциями чтения и записи отдельно. Код реализации Threadsafezoo Code of Zoo Class выглядит следующим образом:
public Class Threadsafezoo {private final ReadWritelock ReadWritelock = new ReenterTreadWriteLock (); Защищенная окончательная блокировка readlock = readwritelock.readlock (); Защищенный окончательный Lock writeLock = readwritelock.writelock (); частный список <Animal> Animals = new ArrayList <> (); частный список <AnveryAddedListener> слушатели = new ArrayList <> (); public void addanimal (животное животное) {// Добавить животное в список животных. СЛУШАЕТСЯТИЧЕСКИЙ. lockThis.writelock.unlock ();} return Hellocer;} public void unregisterAnimaladdEnterener (AnimalAddedListener Slister) {// Заблокировать список слушателей для написания этого.writelock.lock (); try {// Удалить слушатель из списка зарегистрированных слушателей. lockThis.writelock.unlock ();}} public void notifyAnimaladdellisteners (Animal Animal) {// Заблокировать список слушателей для чтения. reader lockthis.readlock.unlock ();}}}Благодаря такому развертыванию, реализация субъекта может обеспечить безопасность потока, и несколько потоков могут одновременно выпускать уведомления. Но, несмотря на это, есть еще две проблемы соревнований по ресурсам, которые нельзя игнорировать:
Одновременный доступ к каждому слушателю. Несколько потоков могут уведомить слушателя о том, что нужны новые животные, что означает, что слушатель может быть вызван несколькими потоками одновременно.
Одновременный доступ к списку животных. Несколько потоков могут добавлять объекты в список животных одновременно. Если порядок уведомлений оказывает влияние, это может привести к конкуренции за ресурсами, что требует одновременной механизма обработки операции, чтобы избежать этой проблемы. Если зарегистрированный список слушателей получает уведомление о добавлении Animal2, а затем получает уведомление о добавлении Animal1, будет происходить конкуренция за ресурсами. Однако, если добавление Animal1 и Animal2 выполняется различными потоками, также возможно завершить добавление Animal1 до Animal2. В частности, нить 1 добавляет Animal1, прежде чем уведомлять слушателя и блокирует модуль, нить 2 добавляет Animal2 и уведомляет слушателя, а затем нить 1 уведомляет слушателя, что Animal1 был добавлен. Хотя конкуренция за ресурсами может быть проигнорирована, когда порядок последовательности не рассматривается, проблема реальна.
Одновременный доступ к слушателям
Слушатели одновременного доступа могут быть реализованы, обеспечивая безопасность потока слушателей. Придерживаясь духа «самоокупаемости» класса, слушатель имеет «обязательство» обеспечить свою собственную безопасность потока. Например, для подсчета слушателя выше, увеличение или уменьшение чисел животных по нескольким потокам может привести к проблемам безопасности резьбы. Чтобы избежать этой проблемы, расчет чисел животных должен быть атомными операциями (атомные переменные или синхронизация методов). Конкретный код решения заключается в следующем:
Общедоступный класс ThreadsafeCountingAnimaldedListener реализует AnimalAddedListener {частное статическое атомно -животное AnimaldDcount = новый Atomiclong (0);@overridepublic void udvateAnimaldded (животное животное) {// увеличение числа животных.Код решения для синхронизации метода выглядит следующим образом:
Общедоступный класс TounttingAnimaldedListener реализует AnimalAddedListener {Private Static int AnimaldDcount = 0; @OverridePublic Synchronized void udVateAnimaldded (животное животное) {// увеличение количества животных.Следует подчеркнуть, что слушатель должен обеспечить свою собственную безопасность потока. Субъект должен понять внутреннюю логику слушателя, а не просто обеспечить безопасность потока для доступа и изменения слушателя. В противном случае, если несколько субъектов делятся одним и тем же слушателем, каждый класс субъекта должен переписать защитный код. Очевидно, что такой код не достаточно краткий, поэтому безопасность потока необходимо реализовать в классе слушателя.
Заказанные уведомления слушателей
Когда слушатель должен выполнять упорядоченным образом, блокировка чтения и записи не может удовлетворить потребности, и необходимо ввести новый механизм, чтобы убедиться, что порядок вызова функции уведомления соответствовал тому, как животное добавляется в зоопарк. Некоторые люди пытались реализовать его с использованием синхронизации метода, но в соответствии с введением синхронизации метода в документации Oracle можно видеть, что синхронизация метода не обеспечивает управления порядком выполнения операции. Это только гарантирует, что атомные операции не прерваны, и не гарантирует порядок потока в первой первой степени выполнять (FIFO). ReentrantreadWritelock может реализовать такой заказ на выполнение, код заключается в следующем:
public class orsamedthreadsafezoo {private final readwritelock readwritelock = new Reentrantreadwritelock (true); Защищенная окончательная блокировка readlock = readwritelock.readlock (); Защищенный окончательный Lock writeLock = readwritelock.writelock (); частный список <Animal> Animals = new ArrayList <> (); частный список <AnveryAddedListener> слушатели = new ArrayList <> (); public void addanimal (животное животное) {// Добавить животное в список животных. СЛУШАЕТСЯТИЧЕСКИЙ. lockThis.writelock.unlock ();} return Hellocer;} public void unregisterAnimaladdEnterener (AnimalAddedListener Slister) {// Заблокировать список слушателей для написания этого.writelock.lock (); try {// Удалить слушатель из списка зарегистрированных слушателей. lockThis.writelock.unlock ();}} public void notifyAnimaladdellisteners (Animal Animal) {// Заблокировать список слушателей для чтения. reader lockthis.readlock.unlock ();}}}Таким образом, зарегистрируйте, не регистрируют и уведомляют функции получат разрешения на чтение и записи блокировки в порядке первого в-первого (FIFO). Например, поток 1 регистрирует слушатель, поток 2 пытается уведомить зарегистрированного слушателя после начала операции регистрации, поток 3 пытается уведомить зарегистрированного слушателя, когда поток 2 ждет блокировки только для чтения, принятие метода справедливого порядка, поток 1 завершает операцию регистрации, а затем нить 2 может уведомлять слушатель, и, наконец, нить 3 уведомляет слушатель. Это гарантирует, что порядок выполнения и начальный порядок действия являются последовательными.
Если применяется синхронизация метода, хотя поток 2 очереди в очереди, чтобы занять ресурсы, поток 3 все еще может получить блокировку ресурса перед потоком 2, и нельзя гарантировать, что поток 2 уведомляет слушателя сначала, чем поток 3. Ключ к проблеме: метод справедливого порядка может гарантировать, что потоки выполняются в порядке, в котором применяются ресурсы. Механизм заказов чтения и записи замков очень сложный. Вам следует обратиться к официальной документации ReenterTreadWritelock, чтобы гарантировать, что логика блокировки достаточной для решения проблемы.
До сих пор была реализована безопасность потоков, и преимущества и недостатки извлечения логики темы и инкапсулирования класса Mixin в повторяемые кодовые единицы будут введены в следующих главах.
Логика темы инкапсулирована в класс Mixin
Очень привлекательно инкапсулировать вышеупомянутую реализацию дизайна образца наблюдателя в целевом классе Mixin. Вообще говоря, наблюдатели в режиме наблюдателя содержат коллекцию зарегистрированных слушателей; Функции регистрации ответственности за регистрацию новых слушателей; Функции UNREGISTER, ответственные за отзыв зарегистрированных функций UNREGISTER и уведомление функций, ответственных за уведомление слушателей. Для приведенного выше примера зоопарка все остальные операции класса зоопарка, за исключением того, что список животных необходим для проблемы, чтобы реализовать логику субъекта.
Случай класса Mixin показан ниже. Следует отметить, что для того, чтобы сделать код более кратким, код безопасности потока удаляется здесь:
Общедоступный класс. {// Удалить слушателя из списка зарегистрированных прослушиваний this.listeners.remove (слушатель);} public void notifylisteners (Consumer <? Super Sliedertype> algorithm) {// выполнять какую -то функцию на каждом из слушателей.Поскольку информация о интерфейсе зарегистрированного типа слушателя не предоставлена, конкретный слушатель не может быть уведомлен напрямую, поэтому необходимо обеспечить универсальность функции уведомления и позволить клиенту добавлять некоторые функции, такие как принятие соответствия параметров общих типов параметров, которые будут применимы к каждому слушателю. Конкретный код реализации выглядит следующим образом:
Общедоступный класс ZoousingMixin Extends ObservablesBjectMixin <AnveryAddedListener> {частный список <Animal> Animals = new ArrayList <> (); Public void Addanimal (Animal Animal) {// Добавить животное в список животных. Слушатель.Самым большим преимуществом технологии класса MixIn является инкапсуляция предмета, оснащенного наблюдателем, в повторяемый класс, а не повторять логику в каждом классе субъекта. Кроме того, этот метод упрощает реализацию класса зоопарка, только хранение информации о животных без учета того, как хранить и уведомлять слушателей.
Однако использование классов Mixin не просто преимущество. Например, что, если вы хотите хранить несколько типов слушателей? Например, также необходимо хранить тип слушателя AnimalRemovedListener. Класс Mixin - это абстрактный класс. Несколько абстрактных классов не могут быть унаследованы одновременно в Java, и класс Mixin не может быть реализован с использованием интерфейса. Это связано с тем, что интерфейс не содержит состояния, и состояние в режиме наблюдателя необходимо использовать для сохранения зарегистрированного списка прослушивателей.
Одним из решений является создание Zoolistener для слушателя, которое будет уведомлено при увеличении и уменьшении животных. Код выглядит так:
Публичный интерфейс Zoolistener {public void onanimaladded (животное животное); public void onamalremoved (животное животное);}Таким образом, вы можете использовать этот интерфейс для реализации мониторинга различных изменений в состоянии зоопарка, используя тип слушателя:
Общедоступный класс ZoousingMixin Extends ObservablesBjectMixin <Zoolistener> {Private List <Animal> Animals = new ArrayList <> (); Public void Addanimal (животное животное) {// Добавить животное в список животных nathis.animals.add (Animal); // Уведомление о списке зарегистрированных слушателей. Слушатель.nanimaladded (Animal));} public void removeanimal (животное животное) {// Удалить животное из списка животных this.animals.remove (Animal); // Уведомить список зарегистрированных слушателей.Комбинирование нескольких типов слушателей в один интерфейс слушателя решает проблему, упомянутая выше, но все еще есть недостатки, которые будут подробно обсуждаться в следующих главах.
Многометодный слушатель и адаптер
В приведенном выше методе, если интерфейс слушателя реализует слишком много функций, интерфейс будет слишком многословным. Например, Swing Mouseelistener содержит 5 необходимых функций. Хотя вы можете использовать только одну из них, вы должны добавить эти 5 функций, если вы используете событие щелчка мыши. С большей вероятностью использовать пустые функции тела для реализации оставшихся функций, что, несомненно, принесет ненужную путаницу в код.
Одним из решений является создание адаптера (концепция поступает из шаблона адаптера, предложенной GOF). Операция интерфейса слушателя реализована в форме абстрактных функций для наследования конкретного класса слушателя. Таким образом, конкретный класс слушателя может выбрать необходимые в нем функции, и использовать операции по умолчанию для функций, не необходимых для адаптера. Например, в классе Zoolistener в приведенном выше примере создайте Zooadapter (правила именования адаптера соответствуют слушателю, вам нужно только изменить слушателя в имени класса на адаптер), код следующим образом:
Общедоступный класс Zooadapter реализует Zoolistener {@OverridePublic void onAnimaladded (животное животное) {} @OverridePublic void onanimalRemoved (животное животное) {}}На первый взгляд, этот класс адаптера незначителен, но удобство, которое он приносит, не может быть недооценено. Например, для следующих конкретных классов просто выберите полезные для них функции:
Общедоступный класс Nameprinterzooadapter Extends Zooadapter {@OverridePublic void onAnimaladded (животное животное) {// печатать имя животного, которое было добавлено.Есть две альтернативы, которые также могут реализовать функции класса адаптера: одна из них - использовать функцию по умолчанию; Другой - объединить интерфейс слушателя и класс адаптера в конкретный класс. Функция по умолчанию вновь предложена Java 8, что позволяет разработчикам предоставлять методы реализации по умолчанию (защита) в интерфейсе.
Это обновление в библиотеке Java в основном для облегчения разработчиков реализовать расширения программ без изменения старой версии кода, поэтому этот метод следует использовать с осторожностью. После того, как он использовал его много раз, некоторые разработчики будут чувствовать, что код, написанный таким образом, недостаточно профессионален, и некоторые разработчики считают, что это особенность Java 8. Независимо от того, что им нужно понять, каково первоначальное намерение этой технологии, а затем решать, использовать ли ее на основе конкретных вопросов. Код интерфейса Zoolistener, реализованный с использованием функции по умолчанию, выглядит следующим образом:
Публичный интерфейс Zoolistener {по умолчанию public void onanimaladded (животное животное) {} по умолчанию public void onanimalremovioved (животное животное) {}}Используя функции по умолчанию, реализация конкретных классов интерфейса не нужно реализовать все функции в интерфейсе, а вместо этого выборочно реализовать необходимые функции. Хотя это относительно простое решение проблемы расширения интерфейса, разработчики должны уделять больше внимания при его использовании.
Второе решение состоит в том, чтобы упростить режим наблюдателя, пропустить интерфейс слушателя и использовать конкретные классы для реализации функций слушателя. Например, интерфейс Zoolistener становится следующим:
Public Class Zoolistener {public void onanimaladded (животное животное) {} public void onanimalremoved (животное животное) {}}Это решение упрощает иерархию шаблона наблюдателя, но оно не применимо ко всем случаям, потому что, если интерфейс слушателя объединен в конкретный класс, конкретный слушатель не может реализовать несколько интерфейсов прослушивания. Например, если интерфейсы AnimalAddedListener и AnimalRemovedListener записаны в одном и том же конкретном классе, то один специфический слушатель не может внедрить оба интерфейса одновременно. Кроме того, намерение интерфейса слушателя становится более очевидным, чем у конкретного класса. Очевидно, что первое состоит в том, чтобы предоставить интерфейсы для других классов, но последнее не так очевидно.
Без соответствующей документации разработчик не будет знать, что уже есть класс, который играет роль интерфейса и реализует все его соответствующие функции. Кроме того, имя класса не содержит адаптеров, поскольку класс не вписывается в определенный интерфейс, поэтому имя класса не подразумевает это намерение. Подводя итог, конкретная проблема требует выбора конкретного метода, и ни один метод не является всемогущим.
Прежде чем мы начнем следующую главу, важно упомянуть, что адаптеры распространены в режиме наблюдения, особенно в старых версиях кода Java. Swing API реализован на основе адаптеров, так как многие старые приложения используют в шаблоне наблюдателя в Java 5 и Java 6. Слушатель в случае зоопарка может не требовать адаптера, но он должен понимать цель адаптера и его применения, потому что мы можем использовать его в существующем коде. В следующей главе будут представлены интенсивные слушателей. Этот тип слушателя может выполнять трудоемкие операции или совершать асинхронные вызовы и не может немедленно дать возвратное значение.
Сложный и блокирующий слушатель
Одно предположение о шаблоне наблюдателя заключается в том, что когда выполняется функция, называется ряд слушателей, но предполагается, что этот процесс полностью прозрачен для вызывающего абонента. Например, когда клиентский код добавляет животное в зоопарке, неизвестно, что серия слушателей будет вызвана до того, как возвращение будет успешным. Если выполнение слушателя занимает много времени (его время влияет количество слушателей, время выполнения каждого слушателя), код клиента будет знать о побочных эффектах этого простого увеличения операций животных.
Эта статья не может обсудить эту тему всесторонне. Ниже приведены то, на что разработчики должны обратить внимание при вызове сложных слушателей:
Слушатель начинает новую ветку. После того, как новый поток запускается при выполнении логики слушателя в новом потоке, результаты обработки функции слушателя возвращаются, а другие слушатели запускаются.
Субъект запускает новую ветку. В отличие от традиционных линейных итераций списков зарегистрированных слушателей, функция «Уведомление о субъекте» перезапускает новый поток, а затем итерации над списком слушателей в новом потоке. Это позволяет функции уведомления выводить свое возвращаемое значение при выполнении других операций слушателей. Следует отметить, что механизм безопасности потока необходим для обеспечения того, чтобы список слушателей не подвергался одновременному модификациям.
Слушатель очереди вызывает и выполняет функции прослушивания с помощью набора потоков. Инкапсулировать операции слушателя в некоторые функции и очереди их вместо простого итеративного вызова в список слушателей. Как только эти слушатели хранятся в очереди, поток может вытащить один элемент из очереди и выполнить логику прослушивания. Это похоже на проблему производителя-потребителя. Процесс уведомления создает очередь исполняемых функций, которые затем потоки снимают очередь по очереди и выполняют эти функции. Функция должна сохранить время, которое было создано, а не время, которое оно было выполнено для вызова функции слушателя. Например, функция, созданная при вызываемой слушательнице, тогда функция должна сохранить момент времени. Эта функция похожа на следующие операции на Java:
Общедоступный класс AnimalAddedFunctor {Private Final AnimalAddedListener Sulder; частный окончательный параметр животного; public AnimalAddedFunctor (AnimalAddedListener слушатель, параметр животного) {this.listener = слушатель; this.parameter = parameteФункции создаются и сохраняются в очереди и могут быть вызваны в любое время, так что нет необходимости немедленно выполнять соответствующие операции при прохождении списка списка списка. Как только каждая функция, которая активирует слушателя, втянута в очередь, «потребительский поток» вернет эксплуатационные права на код клиента. «Поток потребительского потока» выполнит эти функции в какой -то момент позже, как если бы слушатель активируется функцией уведомления. Эта технология называется привязкой параметров на других языках, что только подходит приведенному выше примеру. Суть технологии состоит в том, чтобы сохранить параметры слушателя, а затем вызов функции execute () напрямую. Если слушатель получает несколько параметров, метод обработки аналогичен.
Следует отметить, что если вы хотите сохранить порядок выполнения слушателя, вам необходимо представить комплексный механизм сортировки. В схеме 1 слушатель активирует новые потоки в обычном порядке, что гарантирует, что слушатель выполняется в порядке регистрации. В схеме 2 очереди поддерживают сортировку, и функции в них будут выполнены в том порядке, в котором они входят в очередь. Проще говоря, разработчики должны обратить внимание на сложность многопоточного выполнения слушателей и тщательно обрабатывать его, чтобы обеспечить их реализацию необходимых функций.
Заключение
До того, как модель наблюдателя была записана в книгу в 1994 году, она уже была основной моделью разработки программного обеспечения, предоставляя много удовлетворительных решений проблем, которые часто возникают при разработке программного обеспечения. Java всегда была лидером в использовании этого шаблона и инкапсулирует этот шаблон в свою стандартную библиотеку, но, учитывая, что Java была обновлена до версии 8, очень необходимо пересмотреть использование классических шаблонов в нем. С появлением выражений Lambda и других новых структур этот «старый» шаблон взял новую жизненную силу. Независимо от того, обрабатывает ли он старые программы или использует этот давний метод для решения новых проблем, особенно для опытных разработчиков Java, шаблон наблюдателя является основным инструментом для разработчиков.
OneApm предоставляет вам комплексные решения для приложений Java. Мы поддерживаем все общие рамки Java и серверы приложений, чтобы помочь вам быстро обнаружить узкие места системы и найти основные причины аномалий. Развертывание на минутных уровнях и мгновенно испытано, мониторинг Java никогда не был проще. Чтобы прочитать больше технических статей, посетите официальный технологический блог Oneapm.
Приведенный выше контент представляет вам, как использовать Java8 для реализации режима наблюдателя (часть 2), я надеюсь, что это будет полезно для всех!