Methods for implementing the Observer pattern using JAVA8 (next) _java

Source: Internet
Author: User

In the previous article to introduce the use of JAVA8 to implement the Observer mode (above), this article continues to introduce JAVA8 observer model related knowledge, the specific content as follows:

Implementation of thread safety

The previous section describes the implementation of the observer pattern in a modern Java environment, albeit simple but complete, but this implementation ignores a critical issue: thread safety. Most open Java applications are multi-threaded, and the observer model is also used for multi-threaded or asynchronous systems. For example, if an external service updates its database, the application also receives the message asynchronously, and then notifies the internal component update in observer mode, rather than the internal component directly registering the listener for external services.

Thread safety in observer mode is primarily concentrated on the body of the pattern, because a thread conflict is likely to occur when modifying the registration listener collection, for example, when one thread tries to add a new listener, and the other tries to add a new animal object, which triggers notification of all registered listeners. Given the sequence, the first thread may or may not have completed registering the new listener before the registered listener receives notification of the new animal. This is a classic threading resource competition case that tells developers that they need a mechanism to keep thread safe.

The simplest solution to this problem is that all operations to access or modify a registered listener list must follow Java synchronization mechanisms, such as:

Public synchronized Animaladdedlistener Registeranimaladdedlistener (Animaladdedlistener listener) {/*...*/} 
Public synchronized void Unregisteranimaladdedlistener (Animaladdedlistener listener) {/*...*/} 

As a result, only one thread at a time can modify or access a registered listener list to successfully avoid resource competition issues, but new problems arise, and the constraints are too stringent (see official pages for more information on synchronized keywords and Java concurrency models). Synchronous access to the listener list can be observed at all times through method synchronization, and registration and revocation listeners are write operations to the listener list, while the Notification Listener Access listener list is read-only. Because notification access is a read operation, multiple notification operations can be performed concurrently.

As a result, as long as no listener registration or revocation registration, any number of concurrent notifications can be executed concurrently without raising a resource contention for the registered listener list. Of course, in other cases, the contention for resources has existed for a long time, in order to solve this problem, the Readwritelock is designed to manage the resource lock of Read and write operations separately. The thread-safe Threadsafezoo Implementation code for the Zoo class is as follows:

public class Threadsafezoo {private final Readwritelock Readwritelock = new Reentrantreadwritelock (); protected final L
Ock Readlock = Readwritelock.readlock ();
Protected final Lock Writelock = Readwritelock.writelock ();
Private list<animal> animals = new arraylist<> ();
Private list<animaladdedlistener> listeners = new arraylist<> (); public void Addanimal (Animal Animal) {//ADD the Animal to the list of Animals this.animals.add (Animal);//Notify The L
IST of registered listeners this.notifyanimaladdedlisteners (animal); Public Animaladdedlistener Registeranimaladdedlistener (Animaladdedlistener Listener) {//Lock the list of listeners fo
R writing This.writeLock.lock (); try {//ADD the listener to the list of registered listeners This.listeners.add (listener);} finally {//Unlock the writ
ER lock This.writeLock.unlock ();
return listener; public void Unregisteranimaladdedlistener (Animaladdedlistener listener) {//Lock the ' list of listeners for writing
This.writeLock.lock (); try {//Remove the listener from the list of the registered listeners This.listeners.remove (listener);} finally {//UNL
Ock The writer lock This.writeLock.unlock (); } public void Notifyanimaladdedlisteners (Animal Animal) {//Lock the ' list of listeners for reading This.readLock.lock (
); try {//Notify each of the listeners in the list of registered listeners This.listeners.forEach (Listener-> listener.u
pdateanimaladded (animal));
finally {//Unlock the Reader lock This.readLock.unlock ();}} }

With this deployment, the subject implementation ensures thread safety and multiple threads can publish notifications at the same time. Nevertheless, there are still two resource competition issues that cannot be overlooked:

Concurrent access to each listener. Multiple threads can simultaneously notify listeners to add an animal, which means that a listener may be called simultaneously by multiple threads.

Concurrent access to the animal list. Multiple threads may add objects to the animal list at the same time, and if the order of notification has an impact, it can lead to resource competition, which requires a concurrency action mechanism to avoid this problem. If the registered listener list adds animal2 after receiving the notification and adds Animal1, the resource competition is generated. However, if the addition of Animal1 and Animal2 is performed by a different thread, it is also possible to complete the Animal1 addition before animal2, specifically, thread 1 adds animal1 and locks the module before notifying the listener, thread 2 adds Animal2 and notifies the listener, Thread 1 then notifies the listener that Animal1 has been added. Although the resource competition can be neglected without considering the order of precedence, the problem is real.

Concurrent access to listeners

Concurrent access listeners can be implemented by ensuring thread safety for the listener. Adhering to the "ego" spirit of the class, the listener has the "obligation" to ensure its own thread safety. For example, for a previously counted listener, the number of threads ascending or descending may cause thread safety problems, to avoid this problem, the animal count must be atomic (atomic or method synchronized), the specific resolution code is as follows:

public class Threadsafecountinganimaladdedlistener implements Animaladdedlistener { 
private static Atomiclong Animalsaddedcount = new Atomiclong (0);
@Override public
void updateanimaladded (Animal Animal) {
//Increment The number of animals
Animalsaddedcount.incrementandget ();
Print the number of animals
System.out.println ("Total animals added:" + animalsaddedcount);
}

Method Synchronization Solution code is as follows:

public class Countinganimaladdedlistener implements Animaladdedlistener { 
private static int animalsaddedcount = 0;
@Override public
synchronized void updateanimaladded (Animal Animal) {
//Increment The number of animals
animalsaddedcount++;
Print the number of animals
System.out.println ("Total animals added:" + animalsaddedcount);
}

To emphasize that the listener should ensure its own thread safety, subject needs to understand the internal logic of the listener, rather than simply ensure that the listener is accessed and modified by thread safety. Otherwise, if multiple subject share the same listener, then each subject class will rewrite thread-safe code, which is clearly not concise enough to implement thread safety within the listener class.

Ordered notifications for listeners

When the listener is required to execute in an orderly manner, the read-write lock can not satisfy the requirement, and a new mechanism is introduced to ensure that the order of notify functions is consistent with the order of animal added to zoo. It has been attempted to be implemented using method synchronization, however, the method synchronization does not provide sequential management of the execution of the operation, depending on the methods in the Oracle documentation. It only guarantees atomic operations, that is, the operation is not interrupted, and does not guarantee the first to execute (FIFO) thread order. Reentrantreadwritelock can implement this order of execution, the code is as follows:

public class Orderedthreadsafezoo {private final Readwritelock Readwritelock = new Reentrantreadwritelock (true); Protec
Ted Final Lock Readlock = Readwritelock.readlock ();
Protected final Lock Writelock = Readwritelock.writelock ();
Private list<animal> animals = new arraylist<> ();
Private list<animaladdedlistener> listeners = new arraylist<> (); public void Addanimal (Animal Animal) {//ADD the Animal to the list of Animals this.animals.add (Animal);//Notify The L
IST of registered listeners this.notifyanimaladdedlisteners (animal); Public Animaladdedlistener Registeranimaladdedlistener (Animaladdedlistener Listener) {//Lock the list of listeners fo
R writing This.writeLock.lock (); try {//ADD the listener to the list of registered listeners This.listeners.add (listener);} finally {//Unlock the writ
ER lock This.writeLock.unlock ();
return listener; } public void Unregisteranimaladdedlistener (Animaladdedlistener listener) {//Lock the list of listenersFor writing this.writeLock.lock (); try {//Remove the listener from the list of the registered listeners This.listeners.remove (listener);} finally {//UNL
Ock The writer lock This.writeLock.unlock (); } public void Notifyanimaladdedlisteners (Animal Animal) {//Lock the ' list of listeners for reading This.readLock.lock (
); try {//Notify each of the listeners in the list of registered listeners This.listeners.forEach (Listener-> listener.u
pdateanimaladded (animal));
finally {//Unlock the Reader lock This.readLock.unlock ();}} }

In this way, the register, unregister, and notify functions will get read-write lock permissions in the order of FIFO. For example, thread 1 registers a listener, thread 2 attempts to notify registered listeners after the start of a registration operation, and thread 3 threads 2 waits for read-only locks while attempting to notify registered listeners, using the Fair-ordering method, thread 1 completes the registration operation, and then thread 2 can notify the listener. The last thread 3 notifies the listener. This ensures that the action is executed in the same order as the start sequence.

If you use method synchronization, although thread 2 queues up for resources first, thread 3 may still get a resource lock than thread 2, and it does not guarantee that thread 2 notifies the listener before thread 3. The crux of the problem: the fair-ordering approach ensures that threads are executed in the order in which the resources are requested. The sequential mechanism of read and write locks is complex and should refer to Reentrantreadwritelock's official documentation to ensure that the logic of the lock is sufficient to resolve the problem.

As of today's thread-safe implementation, the following sections describe the advantages and disadvantages of extracting the logic of the subject and encapsulating its Mixin class as a repeatable code unit.

Subject logic encapsulated to Mixin class

It is attractive to encapsulate the above observer pattern design implementation into the Mixin class of the target. Typically, observers in observer mode contain a collection of registered listeners, register functions responsible for registering new listeners, unregister functions to unregister, and notify functions responsible for notifying listeners. For the above example of zoos, the zoo class except the list of animals is a problem, and all other actions are designed to implement the logic of the subject.

The case for the Mixin class is as follows: to make the code simpler, here's how to get rid of thread-safe code.

Public abstract class Observablesubjectmixin<listenertype> { 
private list<listenertype> listeners = New Arraylist<> ();
Public Listenertype Registerlistener (Listenertype listener) {
//ADD the listener to the list of registered listeners
This.listeners.add (listener);
return listener;
}
public void Unregisteranimaladdedlistener (Listenertype listener) {
//Remove the listener from the list of the Regist Ered Listeners
this.listeners.remove (listener);
}
public void Notifylisteners (consumer<. Super listenertype> algorithm) {
//Execute some function on each of th E Listeners
This.listeners.forEach (algorithm);
}

Because there is no interface information for the listener type being registered, it is not possible to directly notify a particular listener, so it is necessary to ensure the generality of the notification function, allowing the client to add some functionality, such as accepting parameter matching for generic parameter types to apply to each listener, with the following implementation code:

public class Zoousingmixin extends observablesubjectmixin<animaladdedlistener> { 
private list<animal > animals = new arraylist<> ();
public void Addanimal (Animal Animal) {
//ADD the Animal to the list of Animals
this.animals.add (Animal);
Notify the list of registered listeners
This.notifylisteners ((listener)-> listener.updateanimaladded (animal ));
}
}

The great advantage of the Mixin class technology is that the subject of the observer pattern is encapsulated into a reusable class, rather than repeated in each subject class. In addition, this approach makes the implementation of the zoo class simpler and requires only storing animal information, without having to consider how to store and notify listeners.

However, the use of the Mixin class is not only an advantage. For example, what if you want to store multiple types of listeners? For example, you also need to store the listener type Animalremovedlistener. The Mixin class is an abstract class and cannot inherit multiple abstract classes at the same time in Java, and the Mixin class cannot be implemented using an interface instead, because the interface does not contain state, and the State in observer mode needs to save the list of registered listeners.

One of the solutions is to create a listener type Zoolistener that will be notified when an animal grows and decreases, as shown in the following code:

Public interface Zoolistener {public 
void onanimaladded (Animal Animal);
public void onanimalremoved (Animal Animal);
}

This enables you to use this interface to monitor the various changes in the zoo state using a listener type:

public class Zoousingmixin extends observablesubjectmixin<zoolistener> { 
private list<animal> animals = new arraylist<> ();
public void Addanimal (Animal Animal) {
//ADD the Animal to the list of Animals
this.animals.add (Animal);
Notify the list of registered listeners
This.notifylisteners ((listener)-> listener.onanimaladded (animal)); c7/>} public
void Removeanimal (Animal Animal) {
//Remove the Animal from the list of animals
THIS.ANIMALS.R Emove (animal);
Notify the list of registered listeners
This.notifylisteners ((listener)-> listener.onanimalremoved (animal) );
}
}

Merging multiple listener types into one listener interface does solve the problem mentioned above, but there are still deficiencies, which are discussed in more detail in the following sections.

Multi-method Listeners and Adapters

In the above method, the interface of the listener contains too many functions, and the interface is too verbose, for example, Swing MouseListener contains 5 necessary functions. Although only one of them may be used, it is necessary to add the 5 functions to the mouse click event, and more likely to use the empty function body to implement the remainder of the function, which will undoubtedly cause unnecessary confusion to the code.

One solution is to create the adapter (the concept comes from the adapter pattern presented by GOF), which implements the listener interface as an abstract function in the adapter for the specific listener class to inherit. In this way, the specific listener class can select the function that it needs, and use the default action for adapter functions that are not needed. For example, the Zoolistener class in the above example, create the Zooadapter (adapter naming rules are consistent with the listener, just change the listener in the class name to adapter), and the code is as follows:

public class Zooadapter implements Zoolistener { 
@Override public
void onanimaladded (Animal Animal) {}
@ Override public
void onanimalremoved (Animal Animal) {}
}

At first glance, this adapter class is trivial, but the convenience it brings is not negligible. For example, for the following specific classes, simply select the function that is useful for the implementation:

public class Nameprinterzooadapter extends Zooadapter { 
@Override public
void onanimaladded (Animal Animal) {
   //Print The name of the animal that is added
System.out.println ("added animal named" + Animal.getname ());
}
}

There are two alternatives to implement the function of the adapter class: One is to use the default function, and the other is to merge the Listener interface and adapter class into a specific class. The default function is Java8, which allows developers to provide default (defensive) implementations in the interface.

This update to the Java library is primarily to make it easier for developers to implement program extensions without changing the old version of the code, so this method should be used with caution. Some developers use many times, it will feel that the code is not professional, and developers think that this is the characteristics of the JAVA8, in any case, need to understand the purpose of the technology proposed, and then combined with specific issues to decide whether to use. The Zoolistener interface code implemented using the default function is shown below:

Public interface Zoolistener { 
default public void onanimaladded (Animal Animal) {}
default public void Onanimalre Moved (Animal Animal) {}
}

By using the default function, you implement the concrete class of the interface without implementing all of the functions in the interface, but selectively implementing the required functions. Although this is a more concise solution to the problem of interface expansion, developers should pay more attention when using it.

The second scenario is to simplify the observer pattern, omit the listener interface, and implement the listener's function with specific classes. For example, the Zoolistener interface becomes the following:

public class Zoolistener {public 
void onanimaladded (Animal Animal) {} public
void onanimalremoved (Animal Animal ) {}
}

This scheme simplifies the hierarchy of the observer pattern, but it is not applicable in all cases, because if the listener interface is merged into a specific class, the listener will not be able to implement multiple listener interfaces. For example, if the Animaladdedlistener and Animalremovedlistener interfaces are written in the same concrete class, then a single specific listener cannot implement both interfaces at the same time. In addition, the listener interface's intent is more obvious than the specific class, and it is obvious that the former provides an interface for other classes, but the latter is not so obvious.

Without the proper documentation, developers will not know that a class already has the role of an interface, implementing all of its corresponding functions. In addition, the class name does not contain adapter because the class does not fit on an interface, so the class name does not specifically imply this intent. To sum up, specific problems need to choose a specific method, and no way is omnipotent.

Before starting the next chapter, it is important to mention that adapters are common in observation mode, especially in older versions of Java code. The Swing API is implemented on an adapter basis, just as many old applications are used in the observer pattern in JAVA5 and Java6. The listener in the zoo case may not require an adapter, but it needs to know the purpose of the adapter and its application because we can use it in existing code. The following section introduces a time-complicated listener that may perform time-consuming operations or make asynchronous calls and cannot immediately give a return value.

Complex & Blocking Listeners

One assumption about the observer pattern is that a series of listeners is invoked when a function is executed, but assumes that the process is completely transparent to the caller. For example, when the client code adds animal to the zoo, it is not known to invoke a series of listeners before returning the added success. If the listener's execution takes longer (its time is affected by the number of listeners, per listener execution time), the client code will perceive the time side effects of this simple increase in animal operations.

This article does not cover this topic in all its aspects, and the following are some of the things developers should be aware of when calling complex listeners:

The listener starts a new thread. When the new thread starts, it performs the listener logic in the new thread, returning the processing result of the listener function, and running the other listener execution.

Subject starts a new thread. Unlike traditional linear iterations of registered listener lists, the subject notify function restarts a new thread and then iterates over the listener list in a new thread. This allows the Notify function to output its return value while performing other listener actions. Note that a thread-safe mechanism is needed to ensure that the listener list does not make concurrent modifications.

The queue listener calls and takes a set of threads to perform the listening function. Encapsulate the listener action in some functions and queue the functions, rather than simply iterating over the call listener list. Once these listeners are stored in the queue, the thread can eject a single element from the queue and perform its listening logic. This is similar to producer-consumer issues, where the Notify process produces executable function queues, and then the threads take them out of the queue and execute them sequentially, and the function needs to store the time created rather than the time it takes for the listener function to call. For example, when the listener is invoked to create a function, the function needs to store that point in time, similar to the following actions in Java:

public class Animaladdedfunctor { 
private final Animaladdedlistener listener;
Private final Animal parameter;
Public Animaladdedfunctor (Animaladdedlistener Listener, Animal parameter) {
This.listener = listener;
this.parameter = parameter;
}
public void Execute () {
//execute the listener with the parameter provided during creation
This.listener.updateA Nimaladded (This.parameter);
}

Functions are created and saved in a queue and can be invoked at any time, so that you do not have to perform their corresponding actions immediately when traversing the listener list. Once every function that activates the listener is pressed into the queue, the consumer thread returns the operation rights to the client code. At some point in time, the consumer thread will perform these functions, just as it would if the listener was activated by the Notify function. This technique is called as a parameter binding in other languages, just right for the example above, the essence of the technique is to save the parameters of the listener, and the Execute () function is called directly. If the listener receives more than one parameter, the processing method is similar.

It should be noted that if you want to save the order in which listeners are executed, you need to introduce a comprehensive sorting mechanism. In scenario one, the listener activates the new thread in the normal order, which ensures that the listener executes in the order in which it is registered. In scenario two, queues support sorting, and functions are executed in the order in which they enter the queue. Simply put, developers need to focus on the complexity of the listener's multithreaded execution, and be careful with it to ensure that the required functionality is achieved.

Conclusion

Before the Observer model was written into the book in 1994, it was already the mainstream software design model, providing many satisfying solutions to the problems that often arise in software design. Java has always been the leader in using this pattern, encapsulating it in its standard library, but given that Java has been updated to version 8, it is necessary to re-examine the use of classic patterns. With the advent of lambda expressions and other new structures, this "old" pattern has new life. Whether it's dealing with old programs or using this age-old approach to solve new problems, the observer model is a major tool for developers, especially for seasoned Java developers.

ONEAPM provides you with End-to-end Java application Solutions, and we support all common Java frameworks and application servers to help you quickly identify system bottlenecks and locate the root cause of the anomalies. Minute-level deployment, instant experience, and Java monitoring has never been simpler. To read more technical articles, visit ONEAPM's official technical blog.

The above content for you to introduce the use of JAVA8 to implement the Observer mode (next), I hope to help you!

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.